@team-agent/installer 0.5.102 → 0.5.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/models.rs +92 -226
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/compiler/tests.rs +19 -27
- package/crates/team-agent/src/compiler.rs +11 -105
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +4 -2
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +2 -5
- package/crates/team-agent/src/lifecycle/launch/identity.rs +9 -13
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +1 -25
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +0 -12
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +0 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +7 -19
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +8 -41
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -8
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -4
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -4
- package/crates/team-agent/src/messaging/results.rs +11 -42
- package/crates/team-agent/src/model/enums.rs +0 -9
- package/crates/team-agent/src/model/spec.rs +2 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +7 -8
- package/crates/team-agent/src/provider/adapters/grok.rs +5 -18
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +650 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +15 -24
- package/crates/team-agent/src/provider/wire.rs +2 -29
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +48 -33
- package/skills/team-agent/references/team-agent-operator.md +49 -526
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -290,6 +290,7 @@ pub(crate) fn default_help() -> String {
|
|
|
290
290
|
&["claim-leader", "takeover", "attach-leader"],
|
|
291
291
|
);
|
|
292
292
|
out.push_str("\nWorkflow: `team-agent models --provider pi`, then quick-start/add-agent/start-agent, then `team-agent send AGENT MESSAGE`.\n");
|
|
293
|
+
out.push_str("Model catalogs: pi, cursor_agent, codex, claude, claude_code.\n");
|
|
293
294
|
out.push_str("Provider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
|
|
294
295
|
out.push_str("\nRun `team-agent <command> --help` for command flags.");
|
|
295
296
|
out
|
|
@@ -340,7 +341,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
340
341
|
.to_string(),
|
|
341
342
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
342
343
|
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或原生 tmux/process 采样时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
|
|
343
|
-
Some("models") => "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, and
|
|
344
|
+
Some("models") => "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, display name, and source aliases. Uses each provider's native catalog; Codex uses `codex debug models` and Claude uses SDK stream-json initialize.".to_string(),
|
|
344
345
|
Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
|
|
345
346
|
Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
|
|
346
347
|
Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]".to_string(),
|
|
@@ -3,292 +3,157 @@ use super::{CliError, CmdOutput, CmdResult, ExitCode, ModelsArgs};
|
|
|
3
3
|
use serde_json::{json, Value};
|
|
4
4
|
#[cfg(test)]
|
|
5
5
|
use std::collections::BTreeMap;
|
|
6
|
+
#[cfg(test)]
|
|
6
7
|
use std::path::Path;
|
|
8
|
+
#[cfg(test)]
|
|
7
9
|
use std::time::Duration;
|
|
10
|
+
|
|
11
|
+
#[cfg(test)]
|
|
8
12
|
const CATALOG_TIMEOUT: Duration = Duration::from_secs(10);
|
|
13
|
+
#[cfg(test)]
|
|
9
14
|
const MAX_CATALOG_BYTES: u64 = 1024 * 1024;
|
|
15
|
+
type ModelRecord = crate::provider::model_catalog::ModelRecord;
|
|
10
16
|
|
|
17
|
+
#[cfg(test)]
|
|
11
18
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
12
19
|
enum CatalogFormat {
|
|
13
20
|
Pi,
|
|
14
21
|
CursorAgent,
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
#[derive(Clone, Debug)]
|
|
18
|
-
struct ModelRecord {
|
|
19
|
-
provider: String,
|
|
20
|
-
vendor: String,
|
|
21
|
-
id: String,
|
|
22
|
-
display_name: String,
|
|
23
|
-
default: Option<bool>,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
24
|
pub fn cmd_models(args: &ModelsArgs) -> Result<CmdResult, CliError> {
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
&format!("unsupported model provider {provider:?}"),
|
|
34
|
-
))
|
|
25
|
+
let records = match crate::provider::model_catalog::discover_model_catalog(&args.provider) {
|
|
26
|
+
Ok(records) => records,
|
|
27
|
+
Err(error) => {
|
|
28
|
+
let message = error.message(&args.provider);
|
|
29
|
+
let action = error.action(&args.provider);
|
|
30
|
+
return Ok(failure_with_action(args, &message, &action));
|
|
35
31
|
}
|
|
36
32
|
};
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// Existing injectable Pi boundary retained for deterministic command/timeout tests.
|
|
41
|
-
fn cmd_models_with(
|
|
42
|
-
args: &ModelsArgs,
|
|
43
|
-
program: &Path,
|
|
44
|
-
timeout: Duration,
|
|
45
|
-
max_bytes: u64,
|
|
46
|
-
) -> Result<CmdResult, CliError> {
|
|
47
|
-
cmd_models_with_format(args, program, CatalogFormat::Pi, timeout, max_bytes)
|
|
33
|
+
render_catalog(args, records)
|
|
48
34
|
}
|
|
49
35
|
|
|
50
|
-
fn
|
|
51
|
-
args: &ModelsArgs,
|
|
52
|
-
program: &Path,
|
|
53
|
-
format: CatalogFormat,
|
|
54
|
-
timeout: Duration,
|
|
55
|
-
max_bytes: u64,
|
|
56
|
-
) -> Result<CmdResult, CliError> {
|
|
57
|
-
let bytes = match run_catalog(program, timeout, max_bytes) {
|
|
58
|
-
Ok(bytes) => bytes,
|
|
59
|
-
Err(message) => return Ok(failure(args, &catalog_error_prefix(format, message))),
|
|
60
|
-
};
|
|
61
|
-
let all = match parse_catalog(&bytes, format) {
|
|
62
|
-
Ok(models) => models,
|
|
63
|
-
Err(message) => return Ok(failure(args, &message)),
|
|
64
|
-
};
|
|
36
|
+
fn render_catalog(args: &ModelsArgs, all: Vec<ModelRecord>) -> Result<CmdResult, CliError> {
|
|
65
37
|
let visible = args.search.as_deref().map_or_else(
|
|
66
38
|
|| all.clone(),
|
|
67
|
-
|search|
|
|
68
|
-
all.iter()
|
|
69
|
-
.filter(|model| model_matches(model, search))
|
|
70
|
-
.cloned()
|
|
71
|
-
.collect::<Vec<_>>()
|
|
72
|
-
},
|
|
39
|
+
|search| all.iter().filter(|model| crate::provider::model_catalog::model_matches(model, search)).cloned().collect(),
|
|
73
40
|
);
|
|
74
|
-
let current =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"default": model.default,
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
.collect();
|
|
41
|
+
let current = if args.provider == "pi" {
|
|
42
|
+
current_role_model(&all.iter().map(|model| model.id.clone()).collect::<Vec<_>>())
|
|
43
|
+
} else {
|
|
44
|
+
None
|
|
45
|
+
};
|
|
46
|
+
let entries: Vec<Value> = visible.iter().map(|model| json!({
|
|
47
|
+
"role_model": model.id,
|
|
48
|
+
"current": if args.provider == "pi" { json!(current.as_deref() == Some(model.id.as_str())) } else { Value::Null },
|
|
49
|
+
"provider": model.provider,
|
|
50
|
+
"vendor": model.vendor,
|
|
51
|
+
"model_id": model.id,
|
|
52
|
+
"display_name": model.display_name,
|
|
53
|
+
"default": model.default,
|
|
54
|
+
"aliases": model.aliases,
|
|
55
|
+
})).collect();
|
|
93
56
|
let provider = args.provider.as_str();
|
|
94
57
|
let value = json!({
|
|
95
58
|
"schema_version": "models.v1", "ok": true, "provider": provider, "models": entries,
|
|
96
59
|
"auth": "ok", "auth_basis": "catalog_visibility", "current_role_model": current, "search": args.search,
|
|
97
60
|
});
|
|
98
61
|
if args.json {
|
|
99
|
-
let text = serde_json::to_string_pretty(&value)?;
|
|
100
62
|
Ok(CmdResult {
|
|
101
|
-
output: CmdOutput::Human(
|
|
63
|
+
output: CmdOutput::Human(serde_json::to_string_pretty(&value)?),
|
|
102
64
|
exit: ExitCode::Ok,
|
|
103
65
|
as_json: false,
|
|
104
66
|
preserve_json_order: true,
|
|
105
67
|
})
|
|
106
68
|
} else {
|
|
107
|
-
let mut lines = vec![format!(
|
|
108
|
-
"models.v1 | {provider} models (copyable model_id):"
|
|
109
|
-
)];
|
|
69
|
+
let mut lines = vec![format!("models.v1 | {provider} models (copyable model_id):")];
|
|
110
70
|
lines.push("PROVIDER VENDOR MODEL_ID DISPLAY_NAME CURRENT DEFAULT".to_string());
|
|
111
|
-
lines.extend(visible.iter().map(|model|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
model.display_name,
|
|
118
|
-
if format == CatalogFormat::Pi {
|
|
119
|
-
(current.as_deref() == Some(model.id.as_str())).to_string()
|
|
120
|
-
} else {
|
|
121
|
-
"—".to_string()
|
|
122
|
-
},
|
|
123
|
-
model
|
|
124
|
-
.default
|
|
125
|
-
.map_or_else(|| "—".to_string(), |value| value.to_string()),
|
|
126
|
-
)
|
|
127
|
-
}));
|
|
128
|
-
lines.push(format!(
|
|
129
|
-
"auth: ok (catalog_visibility); {} model(s)",
|
|
130
|
-
visible.len()
|
|
131
|
-
));
|
|
71
|
+
lines.extend(visible.iter().map(|model| format!(
|
|
72
|
+
"{} {} {} {} {} {}", model.provider, model.vendor, model.id, model.display_name,
|
|
73
|
+
if args.provider == "pi" { (current.as_deref() == Some(model.id.as_str())).to_string() } else { "—".to_string() },
|
|
74
|
+
model.default.map_or_else(|| "—".to_string(), |value| value.to_string()),
|
|
75
|
+
)));
|
|
76
|
+
lines.push(format!("auth: ok (catalog_visibility); {} model(s)", visible.len()));
|
|
132
77
|
if visible.is_empty() {
|
|
133
|
-
lines.push("No models matched --search/query.".to_string());
|
|
78
|
+
lines.push("No models matched --search/query; rerun without a search to list the full catalog.".to_string());
|
|
134
79
|
}
|
|
135
80
|
Ok(CmdResult::human(lines.join("\n")))
|
|
136
81
|
}
|
|
137
82
|
}
|
|
138
83
|
|
|
84
|
+
/// Existing injectable Pi boundary retained for deterministic command/timeout tests.
|
|
85
|
+
#[cfg(test)]
|
|
86
|
+
fn cmd_models_with(args: &ModelsArgs, program: &Path, timeout: Duration, max_bytes: u64) -> Result<CmdResult, CliError> {
|
|
87
|
+
cmd_models_with_format(args, program, CatalogFormat::Pi, timeout, max_bytes)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
fn cmd_models_with_format(args: &ModelsArgs, program: &Path, format: CatalogFormat, timeout: Duration, max_bytes: u64) -> Result<CmdResult, CliError> {
|
|
92
|
+
let bytes = match run_catalog(program, timeout, max_bytes) {
|
|
93
|
+
Ok(bytes) => bytes,
|
|
94
|
+
Err(message) => return Ok(failure(args, &message)),
|
|
95
|
+
};
|
|
96
|
+
let all = match parse_catalog(&bytes, format) {
|
|
97
|
+
Ok(models) => models,
|
|
98
|
+
Err(message) => return Ok(failure(args, &message)),
|
|
99
|
+
};
|
|
100
|
+
render_catalog(args, all)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[cfg(test)]
|
|
139
104
|
fn parse_catalog(bytes: &[u8], format: CatalogFormat) -> Result<Vec<ModelRecord>, String> {
|
|
140
|
-
match format {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let vendor = id
|
|
147
|
-
.split_once('/')
|
|
148
|
-
.map_or_else(|| "pi".to_string(), |(vendor, _)| vendor.to_string());
|
|
149
|
-
ModelRecord {
|
|
150
|
-
provider: "pi".to_string(),
|
|
151
|
-
vendor,
|
|
152
|
-
display_name: id.clone(),
|
|
153
|
-
id,
|
|
154
|
-
default: None,
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
.collect()
|
|
158
|
-
})
|
|
159
|
-
.map_err(safe_catalog_error),
|
|
160
|
-
CatalogFormat::CursorAgent => parse_cursor_catalog(bytes),
|
|
161
|
-
}
|
|
105
|
+
let provider = match format { CatalogFormat::Pi => "pi", CatalogFormat::CursorAgent => "cursor_agent" };
|
|
106
|
+
let result = match format {
|
|
107
|
+
CatalogFormat::Pi => crate::provider::model_catalog::parse_pi_catalog(bytes),
|
|
108
|
+
CatalogFormat::CursorAgent => crate::provider::model_catalog::parse_cursor_catalog(bytes),
|
|
109
|
+
};
|
|
110
|
+
result.map_err(|error| error.message(provider))
|
|
162
111
|
}
|
|
163
112
|
|
|
113
|
+
#[cfg(test)]
|
|
164
114
|
fn parse_cursor_catalog(bytes: &[u8]) -> Result<Vec<ModelRecord>, String> {
|
|
165
|
-
|
|
166
|
-
.map_err(|
|
|
167
|
-
let mut models = Vec::new();
|
|
168
|
-
for line in text.lines() {
|
|
169
|
-
let line = line.trim();
|
|
170
|
-
if line.is_empty()
|
|
171
|
-
|| line
|
|
172
|
-
.trim_end_matches(':')
|
|
173
|
-
.eq_ignore_ascii_case("available models")
|
|
174
|
-
{
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
let Some((id, display)) = line.split_once(" - ") else {
|
|
178
|
-
return Err("Cursor model catalog contains a malformed row".to_string());
|
|
179
|
-
};
|
|
180
|
-
let id = id.trim();
|
|
181
|
-
if id.is_empty() || id.chars().any(char::is_whitespace) || display.trim().is_empty() {
|
|
182
|
-
return Err("Cursor model catalog contains a malformed row".to_string());
|
|
183
|
-
}
|
|
184
|
-
if models.iter().any(|model: &ModelRecord| model.id == id) {
|
|
185
|
-
return Err("Cursor model catalog contains duplicate model ids".to_string());
|
|
186
|
-
}
|
|
187
|
-
let default = display.trim_end().ends_with("(default)");
|
|
188
|
-
let display_name = display.trim().strip_suffix("(default)").map_or_else(
|
|
189
|
-
|| display.trim().to_string(),
|
|
190
|
-
|name| name.trim().to_string(),
|
|
191
|
-
);
|
|
192
|
-
let vendor = id
|
|
193
|
-
.split_once('-')
|
|
194
|
-
.map_or_else(|| "cursor".to_string(), |(vendor, _)| vendor.to_string());
|
|
195
|
-
models.push(ModelRecord {
|
|
196
|
-
provider: "cursor_agent".to_string(),
|
|
197
|
-
vendor,
|
|
198
|
-
id: id.to_string(),
|
|
199
|
-
display_name,
|
|
200
|
-
default: Some(default),
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
if models.is_empty() {
|
|
204
|
-
return Err("Cursor model catalog is empty".to_string());
|
|
205
|
-
}
|
|
206
|
-
Ok(models)
|
|
115
|
+
crate::provider::model_catalog::parse_cursor_catalog(bytes)
|
|
116
|
+
.map_err(|error| error.message("cursor_agent"))
|
|
207
117
|
}
|
|
208
118
|
|
|
119
|
+
#[cfg(test)]
|
|
209
120
|
fn model_matches(model: &ModelRecord, search: &str) -> bool {
|
|
210
|
-
|
|
211
|
-
model.provider.as_str(),
|
|
212
|
-
model.vendor.as_str(),
|
|
213
|
-
model.id.as_str(),
|
|
214
|
-
model.display_name.as_str(),
|
|
215
|
-
];
|
|
216
|
-
search
|
|
217
|
-
.split_whitespace()
|
|
218
|
-
.map(str::to_lowercase)
|
|
219
|
-
.all(|needle| {
|
|
220
|
-
fields
|
|
221
|
-
.iter()
|
|
222
|
-
.any(|field| field.to_lowercase().contains(needle.as_str()))
|
|
223
|
-
})
|
|
121
|
+
crate::provider::model_catalog::model_matches(model, search)
|
|
224
122
|
}
|
|
225
123
|
|
|
124
|
+
#[cfg(test)]
|
|
226
125
|
fn current_model_for(format: CatalogFormat, models: &[ModelRecord]) -> Option<String> {
|
|
227
126
|
match format {
|
|
228
|
-
CatalogFormat::Pi => current_role_model(
|
|
229
|
-
&models
|
|
230
|
-
.iter()
|
|
231
|
-
.map(|model| model.id.clone())
|
|
232
|
-
.collect::<Vec<_>>(),
|
|
233
|
-
),
|
|
234
|
-
// Cursor's catalog does not expose the active role model; never mark a
|
|
235
|
-
// catalog default as current without an explicit runtime identity.
|
|
127
|
+
CatalogFormat::Pi => current_role_model(&models.iter().map(|model| model.id.clone()).collect::<Vec<_>>()),
|
|
236
128
|
CatalogFormat::CursorAgent => None,
|
|
237
129
|
}
|
|
238
130
|
}
|
|
239
131
|
|
|
240
|
-
|
|
241
|
-
match format {
|
|
242
|
-
CatalogFormat::Pi => message,
|
|
243
|
-
CatalogFormat::CursorAgent => message.replace("Pi", "Cursor"),
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
132
|
+
#[cfg(test)]
|
|
247
133
|
fn failure(args: &ModelsArgs, message: &str) -> CmdResult {
|
|
248
|
-
let executable = if args.provider == "cursor_agent" {
|
|
249
|
-
"agent"
|
|
250
|
-
} else {
|
|
251
|
-
"pi"
|
|
252
|
-
};
|
|
253
134
|
let action = if message.starts_with("unsupported model provider") {
|
|
254
|
-
"choose a supported provider: pi or
|
|
135
|
+
"choose a supported provider: pi, cursor_agent, codex, claude, or claude_code".to_string()
|
|
255
136
|
} else {
|
|
256
|
-
|
|
257
|
-
"
|
|
258
|
-
|
|
259
|
-
|
|
137
|
+
let executable = match args.provider.as_str() {
|
|
138
|
+
"cursor_agent" => "agent",
|
|
139
|
+
"codex" => "codex",
|
|
140
|
+
"claude" | "claude_code" => "claude",
|
|
141
|
+
_ => "pi",
|
|
142
|
+
};
|
|
143
|
+
format!("install or repair the PATH-first `{executable}` executable, then retry `team-agent models --provider {}`", args.provider)
|
|
260
144
|
};
|
|
145
|
+
failure_with_action(args, message, &action)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn failure_with_action(args: &ModelsArgs, message: &str, action: &str) -> CmdResult {
|
|
261
149
|
let value = json!({ "schema_version": "models.v1", "ok": false, "provider": args.provider, "auth": "not_ready", "auth_basis": "catalog_visibility", "models": Value::Array(Vec::new()), "current_role_model": Value::Null, "error": message, "action": action });
|
|
262
150
|
if args.json {
|
|
263
|
-
let text = serde_json::to_string_pretty(&value)
|
|
264
|
-
|
|
265
|
-
CmdResult {
|
|
266
|
-
output: CmdOutput::Human(text),
|
|
267
|
-
exit: ExitCode::Error,
|
|
268
|
-
as_json: false,
|
|
269
|
-
preserve_json_order: true,
|
|
270
|
-
}
|
|
151
|
+
let text = serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{\"schema_version\":\"models.v1\",\"ok\":false}".to_string());
|
|
152
|
+
CmdResult { output: CmdOutput::Human(text), exit: ExitCode::Error, as_json: false, preserve_json_order: true }
|
|
271
153
|
} else {
|
|
272
154
|
CmdResult { output: CmdOutput::Human(format!("models.v1 | error: {message}\naction: {action}\nauth: not_ready (catalog_visibility)")), exit: ExitCode::Error, as_json: false, preserve_json_order: false }
|
|
273
155
|
}
|
|
274
156
|
}
|
|
275
|
-
|
|
276
|
-
fn safe_catalog_error(error: crate::provider::ProviderError) -> String {
|
|
277
|
-
let text = error.to_string();
|
|
278
|
-
if text.contains("not UTF-8") {
|
|
279
|
-
"Pi model catalog is not valid UTF-8".into()
|
|
280
|
-
} else if text.contains("duplicate") {
|
|
281
|
-
"Pi model catalog contains duplicate model ids".into()
|
|
282
|
-
} else if text.contains("header") {
|
|
283
|
-
"Pi model catalog has an unexpected header".into()
|
|
284
|
-
} else if text.contains("row") {
|
|
285
|
-
"Pi model catalog contains a malformed row".into()
|
|
286
|
-
} else if text.contains("empty") || text.contains("no models") {
|
|
287
|
-
"Pi model catalog is empty".into()
|
|
288
|
-
} else {
|
|
289
|
-
"Pi model catalog is invalid".into()
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
157
|
fn current_role_model(models: &[String]) -> Option<String> {
|
|
293
158
|
current_role_model_from(
|
|
294
159
|
models,
|
|
@@ -309,6 +174,7 @@ fn current_role_model_from(
|
|
|
309
174
|
}
|
|
310
175
|
|
|
311
176
|
/// Injectable executable, deadline, and byte-limit boundary for deterministic tests.
|
|
177
|
+
#[cfg(test)]
|
|
312
178
|
fn run_catalog(program: &Path, timeout: Duration, max_bytes: u64) -> Result<Vec<u8>, String> {
|
|
313
179
|
crate::lifecycle::launch::pi_mcp::run_pi_catalog(program, timeout, max_bytes)
|
|
314
180
|
}
|
|
@@ -995,7 +861,7 @@ mod tests {
|
|
|
995
861
|
assert!(text.contains("No models matched --search") && text.contains("auth: ok"));
|
|
996
862
|
let refusal = crate::cli::emit::__test_dispatch(
|
|
997
863
|
"models",
|
|
998
|
-
&["--provider".into(), "
|
|
864
|
+
&["--provider".into(), "cloud".into()],
|
|
999
865
|
Path::new("/tmp"),
|
|
1000
866
|
);
|
|
1001
867
|
assert!(matches!(refusal, Ok(ExitCode::Error)));
|
|
@@ -1095,14 +961,14 @@ mod tests {
|
|
|
1095
961
|
);
|
|
1096
962
|
assert_eq!(
|
|
1097
963
|
crate::cli::run(
|
|
1098
|
-
&["models".into(), "--provider".into(), "
|
|
964
|
+
&["models".into(), "--provider".into(), "cloud".into()],
|
|
1099
965
|
Path::new("/tmp")
|
|
1100
966
|
),
|
|
1101
967
|
ExitCode::Error
|
|
1102
968
|
);
|
|
1103
969
|
assert_eq!(
|
|
1104
970
|
crate::cli::spec::command_spec("models").unwrap().usage,
|
|
1105
|
-
"usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]"
|
|
971
|
+
"usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]"
|
|
1106
972
|
);
|
|
1107
973
|
}
|
|
1108
974
|
|
|
@@ -147,7 +147,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
147
147
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
148
148
|
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
149
149
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
150
|
-
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
150
|
+
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
151
151
|
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
152
152
|
CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
153
153
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
@@ -165,7 +165,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
165
165
|
CommandSpec { name: "copilot", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "copilot" }, summary: "launch a Copilot leader", usage: "usage: team-agent copilot [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
|
|
166
166
|
CommandSpec { name: "grok", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "grok" }, summary: "launch a Grok leader", usage: "usage: team-agent grok [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
|
|
167
167
|
CommandSpec { name: "cursor", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "cursor" }, summary: "launch a Cursor leader", usage: "usage: team-agent cursor [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
|
|
168
|
-
CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "launch a Pi leader", usage: "usage: team-agent pi
|
|
168
|
+
CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "launch a Pi leader", usage: "usage: team-agent pi [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
|
|
169
169
|
|
|
170
170
|
CommandSpec { name: "leaders", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Leaders), summary: "list live, stale, or ambiguous host leaders", usage: "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
171
171
|
CommandSpec { name: "doctor", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "inspect environment and Team runtime health", usage: "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
@@ -221,9 +221,8 @@ fn compile_subscription_without_profile_is_thin_manifest() {
|
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
// Runtime front-matter defaults:
|
|
225
|
-
//
|
|
226
|
-
// (TEAM.md has no `model:` key, only `default_model:`).
|
|
224
|
+
// Runtime front-matter defaults: TEAM.md runtime settings remain available,
|
|
225
|
+
// but its default_model does not fill an omitted role model; leader.model is null.
|
|
227
226
|
const RUNTIME_DEFAULTS_TEAM: &str = "\
|
|
228
227
|
---
|
|
229
228
|
name: doc-team
|
|
@@ -256,7 +255,7 @@ dangerously_skip_permissions: false
|
|
|
256
255
|
Implement bounded tasks.
|
|
257
256
|
";
|
|
258
257
|
|
|
259
|
-
const RUNTIME_DEFAULTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":
|
|
258
|
+
const RUNTIME_DEFAULTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":null,"auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"fast":true,"tick_interval_sec":1,"push_min_interval_sec":3,"stuck_timeout_sec":5},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
260
259
|
|
|
261
260
|
#[test]
|
|
262
261
|
fn compile_runtime_front_matter_defaults_match_python() {
|
|
@@ -269,8 +268,7 @@ fn compile_runtime_front_matter_defaults_match_python() {
|
|
|
269
268
|
assert_eq!(templated_compact_json(&spec), RUNTIME_DEFAULTS_JSON);
|
|
270
269
|
}
|
|
271
270
|
|
|
272
|
-
// provider_models
|
|
273
|
-
// key in provider_models falls back to the `claude` key (compiler.py:317-318).
|
|
271
|
+
// TEAM provider_models do not fill a role whose model is omitted.
|
|
274
272
|
const ALIAS_TEAM: &str = "\
|
|
275
273
|
---
|
|
276
274
|
name: debate-team
|
|
@@ -296,16 +294,16 @@ dangerously_skip_permissions: false
|
|
|
296
294
|
Edit and defend the argument.
|
|
297
295
|
";
|
|
298
296
|
|
|
299
|
-
const ALIAS_JSON: &str = r#"{"version":1,"team":{"name":"debate-team","mode":"supervisor_worker","objective":"Compile thin role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"editor","role":"Editor and Defender","provider":"claude_code","model":
|
|
297
|
+
const ALIAS_JSON: &str = r#"{"version":1,"team":{"name":"debate-team","mode":"supervisor_worker","objective":"Compile thin role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"editor","role":"Editor and Defender","provider":"claude_code","model":null,"auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Edit and defend the argument.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["editor","Editor and Defender"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"editor","rules":[{"id":"route-editor","match":{"assignee":["editor"]},"assign_to":"editor","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-debate-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["editor"],"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"editor","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
300
298
|
|
|
301
299
|
#[test]
|
|
302
|
-
fn
|
|
300
|
+
fn compile_provider_models_do_not_fill_claude_code_role_model() {
|
|
303
301
|
let team = build_team(ALIAS_TEAM, &[("implementer.md", ALIAS_ROLE)], &[]);
|
|
304
302
|
let spec = compile_team(&team).unwrap();
|
|
305
303
|
assert_eq!(templated_compact_json(&spec), ALIAS_JSON);
|
|
306
304
|
}
|
|
307
305
|
|
|
308
|
-
//
|
|
306
|
+
// Omitted role model remains null; the provider supplies its native default.
|
|
309
307
|
const BUILTIN_TEAM: &str = "\
|
|
310
308
|
---
|
|
311
309
|
name: default-model-team
|
|
@@ -329,10 +327,10 @@ tools:
|
|
|
329
327
|
Implement bounded tasks.
|
|
330
328
|
";
|
|
331
329
|
|
|
332
|
-
const BUILTIN_JSON: &str = r#"{"version":1,"team":{"name":"default-model-team","mode":"supervisor_worker","objective":"Compile role docs without model fields.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":
|
|
330
|
+
const BUILTIN_JSON: &str = r#"{"version":1,"team":{"name":"default-model-team","mode":"supervisor_worker","objective":"Compile role docs without model fields.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":null,"auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-default-model-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
333
331
|
|
|
334
332
|
#[test]
|
|
335
|
-
fn
|
|
333
|
+
fn compile_subscription_without_model_keeps_provider_native_default() {
|
|
336
334
|
let team = build_team(BUILTIN_TEAM, &[("implementer.md", BUILTIN_ROLE)], &[]);
|
|
337
335
|
let spec = compile_team(&team).unwrap();
|
|
338
336
|
assert_eq!(templated_compact_json(&spec), BUILTIN_JSON);
|
|
@@ -504,38 +502,32 @@ fn build_layout(parent: &str, leaf: &str, team_md: Option<&str>, agents: AgentsD
|
|
|
504
502
|
// ── A1 model resolution ──
|
|
505
503
|
|
|
506
504
|
#[test]
|
|
507
|
-
fn
|
|
508
|
-
//
|
|
505
|
+
fn team_model_defaults_do_not_fill_role_model() {
|
|
506
|
+
// TEAM default_model and provider_models are not implicit role settings.
|
|
509
507
|
let tm = "---\nname: T\nprovider: codex\ndefault_model: team-y\nprovider_models:\n codex: pm-z\n---\nx\n";
|
|
510
508
|
let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
|
|
511
509
|
let spec = compile_team(&team).unwrap();
|
|
512
|
-
assert_eq!(
|
|
513
|
-
agent0(&spec).get("model").and_then(Value::as_str),
|
|
514
|
-
Some("pm-z")
|
|
515
|
-
);
|
|
510
|
+
assert_eq!(agent0(&spec).get("model"), Some(&Value::Null));
|
|
516
511
|
}
|
|
517
512
|
|
|
518
513
|
#[test]
|
|
519
|
-
fn
|
|
520
|
-
//
|
|
514
|
+
fn team_provider_model_aliases_do_not_fill_role_model() {
|
|
515
|
+
// TEAM provider-model aliases do not affect an omitted role model.
|
|
521
516
|
let tm = "---\nname: T\nprovider: codex\nprovider_models:\n claude_code: cc-v\n---\nx\n";
|
|
522
517
|
let team = build_team(tm, &[("w.md", &role_nomodel("claude"))], &[]);
|
|
523
518
|
let spec = compile_team(&team).unwrap();
|
|
524
|
-
assert_eq!(
|
|
525
|
-
agent0(&spec).get("model").and_then(Value::as_str),
|
|
526
|
-
Some("cc-v")
|
|
527
|
-
);
|
|
519
|
+
assert_eq!(agent0(&spec).get("model"), Some(&Value::Null));
|
|
528
520
|
}
|
|
529
521
|
|
|
530
522
|
#[test]
|
|
531
|
-
fn
|
|
532
|
-
//
|
|
523
|
+
fn claude_role_without_model_keeps_provider_native_default() {
|
|
524
|
+
// A missing explicit model stays null for both Claude provider names.
|
|
533
525
|
for prov in ["claude", "claude_code"] {
|
|
534
526
|
let team = build_team(TM_CODEX, &[("w.md", &role_nomodel(prov))], &[]);
|
|
535
527
|
let spec = compile_team(&team).unwrap();
|
|
536
528
|
assert_eq!(
|
|
537
|
-
agent0(&spec).get("model")
|
|
538
|
-
Some(
|
|
529
|
+
agent0(&spec).get("model"),
|
|
530
|
+
Some(&Value::Null),
|
|
539
531
|
"provider {prov}"
|
|
540
532
|
);
|
|
541
533
|
}
|