mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-04 11:36:44 +00:00
fix: validate env model selection
This commit is contained in:
@@ -6354,7 +6354,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
425. **DONE — config JSON exposes file precedence attribution and unknown config keys are warnings** — fixed 2026-06-03 in `fix: attribute config precedence in JSON`. Runtime config inspection now reports every discovered file with `precedence_rank`, `wins_for_keys`, and `shadowed_keys`, so `.claw/settings.json` overriding legacy `.claw.json` is visible without reimplementing merge order. Unknown keys are tolerated as structured validation warnings, including `claw config <section> --output-format json`, while wrong-type errors still fail. The deprecation-warning path remains deduplicated once per process for text-mode `status`, and JSON config surfaces collect warnings structurally without stderr duplication. Docs in `USAGE.md` now spell out the precedence chain and JSON attribution fields. Regression coverage: `config_json_attributes_precedence_and_shadowed_keys_425`, `config_section_json_tolerates_unknown_keys_as_warnings_425`, `status_deduplicates_config_deprecation_warnings_per_invocation_425`, and the runtime validator unknown-key warning tests.
|
425. **DONE — config JSON exposes file precedence attribution and unknown config keys are warnings** — fixed 2026-06-03 in `fix: attribute config precedence in JSON`. Runtime config inspection now reports every discovered file with `precedence_rank`, `wins_for_keys`, and `shadowed_keys`, so `.claw/settings.json` overriding legacy `.claw.json` is visible without reimplementing merge order. Unknown keys are tolerated as structured validation warnings, including `claw config <section> --output-format json`, while wrong-type errors still fail. The deprecation-warning path remains deduplicated once per process for text-mode `status`, and JSON config surfaces collect warnings structurally without stderr duplication. Docs in `USAGE.md` now spell out the precedence chain and JSON attribution fields. Regression coverage: `config_json_attributes_precedence_and_shadowed_keys_425`, `config_section_json_tolerates_unknown_keys_as_warnings_425`, `status_deduplicates_config_deprecation_warnings_per_invocation_425`, and the runtime validator unknown-key warning tests.
|
||||||
|
|
||||||
|
|
||||||
426. **`ANTHROPIC_MODEL` env var bypasses the `invalid_model_syntax` validator that `--model` enforces — bogus model strings are accepted with `status:"ok"`, deferred-failing only when the first API call is made** — dogfooded 2026-05-11 by Jobdori on `3730b459` in response to Clawhip pinpoint nudge at `1503245298800136296`. Reproduction (asymmetric validation): `claw --model bogus-model-xyz status --output-format json` returns `kind:"invalid_model_syntax"` exit 1; `ANTHROPIC_MODEL=bogus-model-xyz claw status --output-format json` returns `model:"bogus-model-xyz", model_raw:"bogus-model-xyz", model_source:"env", status:"ok"` — the doctor surface lies that the configured model is valid when it is not. The bogus model only manifests as a failure when the first prompt fires and the API rejects it with 404/400. Three sibling discoveries in the same probe: (a) **alias indirection invisible**: `ANTHROPIC_MODEL=opus claw status --output-format json` returns `model:"claude-opus-4-6", model_raw:"opus", model_source:"env"` — the `opus` alias resolves to `claude-opus-4-6` (the *previous* frontier, not the current `claude-opus-4-7` released 2026-04-16). Users typing `opus` get yesterday's model with no warning. (b) **`CLAW_MODEL` env var silently ignored**: `CLAW_MODEL=opus claw status` shows `model:"claude-opus-4-6" model_source:"default"` — the `CLAW_MODEL` env var (the project-namespaced equivalent that users expect) does not exist; only `ANTHROPIC_MODEL` is honored. No warning when a `CLAW_*` env var that looks like it should work is set. (c) **`ANTHROPIC_DEFAULT_MODEL` also silently ignored**: the longer-named env var that some Anthropic SDKs use is not recognized. **Required fix shape:** (a) symmetric validation: `ANTHROPIC_MODEL` env value must pass the same `invalid_model_syntax` check that `--model` does, and `claw status` must return `kind:"invalid_model"` / `status:"warn"` (not `status:"ok"`) when the resolved model is unrecognized; (b) expose alias resolution in `status`: add `model_alias_resolved_to:string|null` field so automation can see `opus → claude-opus-4-6`; (c) bump the `opus` alias to `claude-opus-4-7` (current frontier) or document the alias-to-version mapping policy explicitly; (d) accept `CLAW_MODEL` and `ANTHROPIC_DEFAULT_MODEL` env vars with parity to `ANTHROPIC_MODEL`, OR emit a warning when those env vars are set but unrecognized. **Why this matters:** the most common automation pattern is `export ANTHROPIC_MODEL=...` in a shell rc file. Bogus values pass silently, alias indirection hides the actual model in use, and `CLAW_MODEL` looking like a working name but doing nothing is a footgun. Cross-references #424 (bare canonical names rejected at validator level) — together #424 + #426 make model selection inconsistent across CLI flag, env var, and alias paths. Source: Jobdori live dogfood, `3730b459`, 2026-05-11.
|
426. **DONE — environment model selection is validated and status exposes alias/env provenance** — fixed 2026-06-03 in `fix: validate env model selection`. `CLAW_MODEL`, `ANTHROPIC_MODEL`, and `ANTHROPIC_DEFAULT_MODEL` now share the same env-model path before config/default fallback; prompt/REPL startup validates the resolved model before provider construction; and `status --output-format json` reports invalid env/config models as `status:"warn"` with `model_validation_error_kind:"invalid_model"` while preserving workspace/config/sandbox context. Status JSON now includes `model_alias_resolved_to` and `model_env_var`, making alias expansion and the winning env var auditable. The built-in/default `opus` alias now targets `anthropic/claude-opus-4-7` / `claude-opus-4-7`, with docs updated in `USAGE.md` and `rust/README.md`; the API alias table keeps token-limit metadata for both `claude-opus-4-7` and legacy `claude-opus-4-6`. Regression coverage: `status_json_accepts_namespaced_model_env_and_surfaces_alias_426`, `status_json_warns_on_invalid_model_env_426`, model alias/unit tests, and provider alias tests.
|
||||||
|
|
||||||
|
|
||||||
427. **Subcommand `--help` paths (`resume`, `session`, `compact`) hit the auth gate and trigger config validation before returning static help — `claw resume --help` with no credentials returns `missing_credentials` error instead of help text** — dogfooded 2026-05-11 by Jobdori on `1fecdf09` in response to Clawhip pinpoint nudge at `1503252843669491892`. Reproduction (no env vars, isolated `CLAW_CONFIG_HOME`): `claw resume --help` returns `{"error":"missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY..."}` instead of usage text. Same for `claw session --help`, `claw compact --help`. By contrast, `claw prompt --help` and `claw --help` (top-level) return proper usage text without auth. Even worse: with a broken `.claw.json` discovered up the parent directory tree (e.g., `mcpServers.missing-command: missing string field command`), the subcommand `--help` paths fail with `[error-kind: unknown]` from config validation — config load is happening before `--help` is parsed. **Sibling exit-code bug:** `claw resume --help --output-format json` returns `kind:"missing_credentials"` but exits **0** (the exit-code parity bug from #422 reproduces on this path too — only `cli_parse` exits 1 consistently). **Sibling: `claw resume <bogus-id>` should be local-only** but also hits `missing_credentials` — `resume` of a session that doesn't exist on disk should return `kind:"session_not_found"` from a local lookup, not require API credentials. Same class as ROADMAP #357 (session list requires creds) and #369 (session help/fork require credentials) — now confirmed for `resume`. **Required fix shape:** (a) `--help` MUST short-circuit before any auth check, config load, or session resolution — emit static usage text from a compiled-in string table, no I/O; (b) `resume <id>` must check the local session store first; if the id is absent on disk, emit `kind:"session_not_found"` with `sessions_dir` field; only require auth when resuming a known-on-disk session that requires re-establishing API context; (c) ensure exit code 1 for all error envelopes including `missing_credentials` returned from a `--help` path that should never have reached the auth gate; (d) regression test: with empty `CLAW_CONFIG_HOME` and no env vars, every `claw <subcommand> --help` returns usage text on stdout, exit 0, no `kind:*_error` envelope. **Why this matters:** `--help` is the universal CLI discovery primitive. Failing `--help` because of missing API credentials or broken config files makes claw undiscoverable to users debugging an already-broken setup. Cross-references #357 (session list), #369 (session help/fork), #422 (exit code parity), #108 (subcommand fallthrough). Source: Jobdori live dogfood, `1fecdf09`, 2026-05-11.
|
427. **Subcommand `--help` paths (`resume`, `session`, `compact`) hit the auth gate and trigger config validation before returning static help — `claw resume --help` with no credentials returns `missing_credentials` error instead of help text** — dogfooded 2026-05-11 by Jobdori on `1fecdf09` in response to Clawhip pinpoint nudge at `1503252843669491892`. Reproduction (no env vars, isolated `CLAW_CONFIG_HOME`): `claw resume --help` returns `{"error":"missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY..."}` instead of usage text. Same for `claw session --help`, `claw compact --help`. By contrast, `claw prompt --help` and `claw --help` (top-level) return proper usage text without auth. Even worse: with a broken `.claw.json` discovered up the parent directory tree (e.g., `mcpServers.missing-command: missing string field command`), the subcommand `--help` paths fail with `[error-kind: unknown]` from config validation — config load is happening before `--help` is parsed. **Sibling exit-code bug:** `claw resume --help --output-format json` returns `kind:"missing_credentials"` but exits **0** (the exit-code parity bug from #422 reproduces on this path too — only `cli_parse` exits 1 consistently). **Sibling: `claw resume <bogus-id>` should be local-only** but also hits `missing_credentials` — `resume` of a session that doesn't exist on disk should return `kind:"session_not_found"` from a local lookup, not require API credentials. Same class as ROADMAP #357 (session list requires creds) and #369 (session help/fork require credentials) — now confirmed for `resume`. **Required fix shape:** (a) `--help` MUST short-circuit before any auth check, config load, or session resolution — emit static usage text from a compiled-in string table, no I/O; (b) `resume <id>` must check the local session store first; if the id is absent on disk, emit `kind:"session_not_found"` with `sessions_dir` field; only require auth when resuming a known-on-disk session that requires re-establishing API context; (c) ensure exit code 1 for all error envelopes including `missing_credentials` returned from a `--help` path that should never have reached the auth gate; (d) regression test: with empty `CLAW_CONFIG_HOME` and no env vars, every `claw <subcommand> --help` returns usage text on stdout, exit 0, no `kind:*_error` envelope. **Why this matters:** `--help` is the universal CLI discovery primitive. Failing `--help` because of missing API credentials or broken config files makes claw undiscoverable to users debugging an already-broken setup. Cross-references #357 (session list), #369 (session help/fork), #422 (exit code parity), #108 (subcommand fallthrough). Source: Jobdori live dogfood, `1fecdf09`, 2026-05-11.
|
||||||
|
|||||||
8
USAGE.md
8
USAGE.md
@@ -203,7 +203,7 @@ Supported permission modes:
|
|||||||
|
|
||||||
Model aliases currently supported by the CLI:
|
Model aliases currently supported by the CLI:
|
||||||
|
|
||||||
- `opus` → `claude-opus-4-6`
|
- `opus` → `claude-opus-4-7`
|
||||||
- `sonnet` → `claude-sonnet-4-6`
|
- `sonnet` → `claude-sonnet-4-6`
|
||||||
- `haiku` → `claude-haiku-4-5-20251213`
|
- `haiku` → `claude-haiku-4-5-20251213`
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ These are the models registered in the built-in alias table with known token lim
|
|||||||
|
|
||||||
| Alias | Resolved model name | Provider | Max output tokens | Context window |
|
| Alias | Resolved model name | Provider | Max output tokens | Context window |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `opus` | `claude-opus-4-6` | Anthropic | 32 000 | 200 000 |
|
| `opus` | `claude-opus-4-7` | Anthropic | 32 000 | 200 000 |
|
||||||
| `sonnet` | `claude-sonnet-4-6` | Anthropic | 64 000 | 200 000 |
|
| `sonnet` | `claude-sonnet-4-6` | Anthropic | 64 000 | 200 000 |
|
||||||
| `haiku` | `claude-haiku-4-5-20251213` | Anthropic | 64 000 | 200 000 |
|
| `haiku` | `claude-haiku-4-5-20251213` | Anthropic | 64 000 | 200 000 |
|
||||||
| `grok` / `grok-3` | `grok-3` | xAI | 64 000 | 131 072 |
|
| `grok` / `grok-3` | `grok-3` | xAI | 64 000 | 131 072 |
|
||||||
@@ -382,7 +382,7 @@ You can add custom aliases in any settings file (`~/.claw/settings.json`, `.claw
|
|||||||
{
|
{
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"fast": "claude-haiku-4-5-20251213",
|
"fast": "claude-haiku-4-5-20251213",
|
||||||
"smart": "claude-opus-4-6",
|
"smart": "claude-opus-4-7",
|
||||||
"cheap": "grok-3-mini"
|
"cheap": "grok-3-mini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,6 +390,8 @@ You can add custom aliases in any settings file (`~/.claw/settings.json`, `.claw
|
|||||||
|
|
||||||
Local project settings override user-level settings. Aliases resolve through the built-in table, so `"fast": "haiku"` also works.
|
Local project settings override user-level settings. Aliases resolve through the built-in table, so `"fast": "haiku"` also works.
|
||||||
|
|
||||||
|
Model selection precedence is CLI flag, environment, config, then default. The environment model slot accepts `CLAW_MODEL`, `ANTHROPIC_MODEL`, and `ANTHROPIC_DEFAULT_MODEL` in that order; aliases from those variables are resolved and validated before provider startup. `claw --output-format json status` exposes `model_raw`, `model_alias_resolved_to`, and `model_env_var` so automation can see the winning value.
|
||||||
|
|
||||||
### How provider detection works
|
### How provider detection works
|
||||||
|
|
||||||
1. If the resolved model name starts with `claude` → Anthropic.
|
1. If the resolved model name starts with `claude` → Anthropic.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ cargo run -p rusty-claude-cli -- --help
|
|||||||
cargo build --workspace
|
cargo build --workspace
|
||||||
|
|
||||||
# Run the interactive REPL
|
# Run the interactive REPL
|
||||||
cargo run -p rusty-claude-cli -- --model claude-opus-4-6
|
cargo run -p rusty-claude-cli -- --model claude-opus-4-7
|
||||||
|
|
||||||
# One-shot prompt
|
# One-shot prompt
|
||||||
cargo run -p rusty-claude-cli -- prompt "explain this codebase"
|
cargo run -p rusty-claude-cli -- prompt "explain this codebase"
|
||||||
@@ -109,7 +109,7 @@ Short names resolve to the latest model versions:
|
|||||||
|
|
||||||
| Alias | Resolves To |
|
| Alias | Resolves To |
|
||||||
|-------|------------|
|
|-------|------------|
|
||||||
| `opus` | `claude-opus-4-6` |
|
| `opus` | `claude-opus-4-7` |
|
||||||
| `sonnet` | `claude-sonnet-4-6` |
|
| `sonnet` | `claude-sonnet-4-6` |
|
||||||
| `haiku` | `claude-haiku-4-5-20251213` |
|
| `haiku` | `claude-haiku-4-5-20251213` |
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ rust/
|
|||||||
- **~20K lines** of Rust
|
- **~20K lines** of Rust
|
||||||
- **9 crates** in workspace
|
- **9 crates** in workspace
|
||||||
- **Binary name:** `claw`
|
- **Binary name:** `claw`
|
||||||
- **Default model:** `claude-opus-4-6`
|
- **Default model:** `claude-opus-4-7`
|
||||||
- **Default permissions:** `danger-full-access`
|
- **Default permissions:** `danger-full-access`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_existing_and_grok_aliases() {
|
fn resolves_existing_and_grok_aliases() {
|
||||||
assert_eq!(resolve_model_alias("opus"), "claude-opus-4-6");
|
assert_eq!(resolve_model_alias("opus"), "claude-opus-4-7");
|
||||||
assert_eq!(resolve_model_alias("grok"), "grok-3");
|
assert_eq!(resolve_model_alias("grok"), "grok-3");
|
||||||
assert_eq!(resolve_model_alias("grok-mini"), "grok-3-mini");
|
assert_eq!(resolve_model_alias("grok-mini"), "grok-3-mini");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ pub fn resolve_model_alias(model: &str) -> String {
|
|||||||
.find_map(|(alias, metadata)| {
|
.find_map(|(alias, metadata)| {
|
||||||
(*alias == lower).then_some(match metadata.provider {
|
(*alias == lower).then_some(match metadata.provider {
|
||||||
ProviderKind::Anthropic => match *alias {
|
ProviderKind::Anthropic => match *alias {
|
||||||
"opus" => "claude-opus-4-6",
|
"opus" => "claude-opus-4-7",
|
||||||
"sonnet" => "claude-sonnet-4-6",
|
"sonnet" => "claude-sonnet-4-6",
|
||||||
"haiku" => "claude-haiku-4-5-20251213",
|
"haiku" => "claude-haiku-4-5-20251213",
|
||||||
_ => trimmed,
|
_ => trimmed,
|
||||||
@@ -620,7 +620,7 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
|||||||
let canonical = resolve_model_alias(model);
|
let canonical = resolve_model_alias(model);
|
||||||
let base_model = canonical.rsplit('/').next().unwrap_or(canonical.as_str());
|
let base_model = canonical.rsplit('/').next().unwrap_or(canonical.as_str());
|
||||||
match base_model {
|
match base_model {
|
||||||
"claude-opus-4-6" => Some(ModelTokenLimit {
|
"claude-opus-4-7" | "claude-opus-4-6" => Some(ModelTokenLimit {
|
||||||
max_output_tokens: 32_000,
|
max_output_tokens: 32_000,
|
||||||
context_window_tokens: 200_000,
|
context_window_tokens: 200_000,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ async fn send_message_preserves_deepseek_reasoning_content_before_text() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn custom_openai_gateway_preserves_slash_model_ids_and_extra_body_params() {
|
async fn local_openai_gateway_strips_routing_prefix_and_preserves_extra_body_params() {
|
||||||
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
|
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
|
||||||
let body = concat!(
|
let body = concat!(
|
||||||
"{",
|
"{",
|
||||||
@@ -211,7 +211,7 @@ async fn custom_openai_gateway_preserves_slash_model_ids_and_extra_body_params()
|
|||||||
let captured = state.lock().await;
|
let captured = state.lock().await;
|
||||||
let request = captured.first().expect("captured request");
|
let request = captured.first().expect("captured request");
|
||||||
let body: serde_json::Value = serde_json::from_str(&request.body).expect("json body");
|
let body: serde_json::Value = serde_json::from_str(&request.body).expect("json body");
|
||||||
assert_eq!(body["model"], json!("openai/gpt-4.1-mini"));
|
assert_eq!(body["model"], json!("gpt-4.1-mini"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
body["web_search_options"],
|
body["web_search_options"],
|
||||||
json!({"search_context_size": "low"})
|
json!({"search_context_size": "low"})
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ use tools::{
|
|||||||
execute_tool, mvp_tool_specs, GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput,
|
execute_tool, mvp_tool_specs, GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_MODEL: &str = "anthropic/claude-opus-4-6";
|
const DEFAULT_MODEL: &str = "anthropic/claude-opus-4-7";
|
||||||
|
|
||||||
/// #148: Model provenance for `claw status` JSON/text output. Records where
|
/// #148: Model provenance for `claw status` JSON/text output. Records where
|
||||||
/// the resolved model string came from so claws don't have to re-read argv
|
/// the resolved model string came from so claws don't have to re-read argv
|
||||||
@@ -77,7 +77,7 @@ const DEFAULT_MODEL: &str = "anthropic/claude-opus-4-6";
|
|||||||
enum ModelSource {
|
enum ModelSource {
|
||||||
/// Explicit `--model` / `--model=` CLI flag.
|
/// Explicit `--model` / `--model=` CLI flag.
|
||||||
Flag,
|
Flag,
|
||||||
/// `ANTHROPIC_MODEL` environment variable (when no flag was passed).
|
/// Runtime model environment variable (when no flag was passed).
|
||||||
Env,
|
Env,
|
||||||
/// `model` key in `.claw.json` / `.claw/settings.json` (when neither
|
/// `model` key in `.claw.json` / `.claw/settings.json` (when neither
|
||||||
/// flag nor env set it).
|
/// flag nor env set it).
|
||||||
@@ -105,6 +105,15 @@ struct ModelProvenance {
|
|||||||
raw: Option<String>,
|
raw: Option<String>,
|
||||||
/// Where the resolved model string originated.
|
/// Where the resolved model string originated.
|
||||||
source: ModelSource,
|
source: ModelSource,
|
||||||
|
/// Alias-expanded target when `raw` differs from `resolved`.
|
||||||
|
alias_resolved_to: Option<String>,
|
||||||
|
/// Environment variable that supplied the model, when source is Env.
|
||||||
|
env_var: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnvModel {
|
||||||
|
name: &'static str,
|
||||||
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModelProvenance {
|
impl ModelProvenance {
|
||||||
@@ -113,49 +122,90 @@ impl ModelProvenance {
|
|||||||
resolved: DEFAULT_MODEL.to_string(),
|
resolved: DEFAULT_MODEL.to_string(),
|
||||||
raw: None,
|
raw: None,
|
||||||
source: ModelSource::Default,
|
source: ModelSource::Default,
|
||||||
|
alias_resolved_to: None,
|
||||||
|
env_var: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_flag(raw: &str) -> Self {
|
fn from_flag(raw: &str, resolved: &str) -> Self {
|
||||||
|
Self::from_resolved(raw, resolved, ModelSource::Flag, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_raw(raw: &str, source: ModelSource, env_var: Option<&str>) -> Self {
|
||||||
|
let resolved = resolve_model_alias_with_config(raw);
|
||||||
|
Self::from_resolved(raw, &resolved, source, env_var)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_resolved(
|
||||||
|
raw: &str,
|
||||||
|
resolved: &str,
|
||||||
|
source: ModelSource,
|
||||||
|
env_var: Option<&str>,
|
||||||
|
) -> Self {
|
||||||
|
let raw_trimmed = raw.trim();
|
||||||
|
let alias_resolved_to = (raw_trimmed != resolved).then(|| resolved.to_string());
|
||||||
Self {
|
Self {
|
||||||
resolved: resolve_model_alias_with_config(raw),
|
resolved: resolved.to_string(),
|
||||||
raw: Some(raw.to_string()),
|
raw: Some(raw.to_string()),
|
||||||
source: ModelSource::Flag,
|
source,
|
||||||
|
alias_resolved_to,
|
||||||
|
env_var: env_var.map(str::to_string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_env_or_config_or_default(cli_model: &str) -> Self {
|
fn from_env_or_config_or_default(cli_model: &str) -> Result<Self, String> {
|
||||||
// Only called when no --model flag was passed. Probe env first,
|
// Only called when no --model flag was passed. Probe env first,
|
||||||
// then config, else fall back to default. Mirrors the logic in
|
// then config, else fall back to default. Mirrors the logic in
|
||||||
// resolve_repl_model() but captures the source.
|
// resolve_repl_model() but captures the source.
|
||||||
if cli_model != DEFAULT_MODEL {
|
if cli_model != DEFAULT_MODEL {
|
||||||
// Already resolved from some prior path; treat as flag.
|
let provenance = Self::from_resolved(cli_model, cli_model, ModelSource::Flag, None);
|
||||||
return Self {
|
provenance.validate()?;
|
||||||
resolved: cli_model.to_string(),
|
return Ok(provenance);
|
||||||
raw: Some(cli_model.to_string()),
|
|
||||||
source: ModelSource::Flag,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if let Some(env_model) = env::var("ANTHROPIC_MODEL")
|
if let Some(env_model) = env_model_for_runtime() {
|
||||||
.ok()
|
let provenance =
|
||||||
.map(|value| value.trim().to_string())
|
Self::from_raw(&env_model.value, ModelSource::Env, Some(env_model.name));
|
||||||
.filter(|value| !value.is_empty())
|
provenance.validate()?;
|
||||||
{
|
return Ok(provenance);
|
||||||
return Self {
|
|
||||||
resolved: resolve_model_alias_with_config(&env_model),
|
|
||||||
raw: Some(env_model),
|
|
||||||
source: ModelSource::Env,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if let Some(config_model) = config_model_for_current_dir() {
|
if let Some(config_model) = config_model_for_current_dir() {
|
||||||
return Self {
|
let provenance = Self::from_raw(&config_model, ModelSource::Config, None);
|
||||||
resolved: resolve_model_alias_with_config(&config_model),
|
provenance.validate()?;
|
||||||
raw: Some(config_model),
|
return Ok(provenance);
|
||||||
source: ModelSource::Config,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Self::default_fallback()
|
Ok(Self::default_fallback())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> Result<(), String> {
|
||||||
|
validate_model_syntax(&self.resolved).map_err(|error| {
|
||||||
|
let source = match self.source {
|
||||||
|
ModelSource::Flag => "--model",
|
||||||
|
ModelSource::Env => self.env_var.as_deref().unwrap_or("environment"),
|
||||||
|
ModelSource::Config => "config model",
|
||||||
|
ModelSource::Default => "default model",
|
||||||
|
};
|
||||||
|
if let Some(raw) = &self.raw {
|
||||||
|
format!(
|
||||||
|
"invalid_model: {source} model `{raw}` is invalid after alias resolution to `{}`.\n{error}",
|
||||||
|
self.resolved
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_model_for_runtime() -> Option<EnvModel> {
|
||||||
|
["CLAW_MODEL", "ANTHROPIC_MODEL", "ANTHROPIC_DEFAULT_MODEL"]
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|name| {
|
||||||
|
env::var(name)
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(|value| EnvModel { name, value })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_tokens_for_model(model: &str) -> u32 {
|
fn max_tokens_for_model(model: &str) -> u32 {
|
||||||
@@ -307,6 +357,8 @@ fn classify_error_kind(message: &str) -> &'static str {
|
|||||||
"missing_flag_value"
|
"missing_flag_value"
|
||||||
} else if message.starts_with("invalid_flag_value:") {
|
} else if message.starts_with("invalid_flag_value:") {
|
||||||
"invalid_flag_value"
|
"invalid_flag_value"
|
||||||
|
} else if message.starts_with("invalid_model:") {
|
||||||
|
"invalid_model"
|
||||||
} else if message.contains("invalid model syntax") {
|
} else if message.contains("invalid model syntax") {
|
||||||
"invalid_model_syntax"
|
"invalid_model_syntax"
|
||||||
} else if message.contains("is not yet implemented") {
|
} else if message.contains("is not yet implemented") {
|
||||||
@@ -625,7 +677,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
||||||
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
|
let resolved_model = resolve_repl_model(model)?;
|
||||||
|
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
||||||
cli.set_reasoning_effort(reasoning_effort);
|
cli.set_reasoning_effort(reasoning_effort);
|
||||||
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
|
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
|
||||||
}
|
}
|
||||||
@@ -2062,7 +2115,7 @@ fn levenshtein_distance(left: &str, right: &str) -> usize {
|
|||||||
|
|
||||||
fn resolve_model_alias(model: &str) -> &str {
|
fn resolve_model_alias(model: &str) -> &str {
|
||||||
match model {
|
match model {
|
||||||
"opus" => "anthropic/claude-opus-4-6",
|
"opus" => "anthropic/claude-opus-4-7",
|
||||||
"sonnet" => "anthropic/claude-sonnet-4-6",
|
"sonnet" => "anthropic/claude-sonnet-4-6",
|
||||||
"haiku" => "anthropic/claude-haiku-4-5-20251213",
|
"haiku" => "anthropic/claude-haiku-4-5-20251213",
|
||||||
_ => model,
|
_ => model,
|
||||||
@@ -2225,21 +2278,37 @@ fn config_model_for_current_dir() -> Option<String> {
|
|||||||
loader.load().ok()?.model().map(ToOwned::to_owned)
|
loader.load().ok()?.model().map(ToOwned::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_repl_model(cli_model: String) -> String {
|
fn resolve_repl_model(cli_model: String) -> Result<String, String> {
|
||||||
if cli_model != DEFAULT_MODEL {
|
Ok(ModelProvenance::from_env_or_config_or_default(&cli_model)?.resolved)
|
||||||
return cli_model;
|
}
|
||||||
}
|
|
||||||
if let Some(env_model) = env::var("ANTHROPIC_MODEL")
|
fn print_model_validation_warning_status(
|
||||||
.ok()
|
error: &str,
|
||||||
.map(|value| value.trim().to_string())
|
usage: StatusUsage,
|
||||||
.filter(|value| !value.is_empty())
|
permission_mode: &str,
|
||||||
{
|
context: &StatusContext,
|
||||||
return resolve_model_alias_with_config(&env_model);
|
allowed_tools: Option<&AllowedToolSet>,
|
||||||
}
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if let Some(config_model) = config_model_for_current_dir() {
|
let kind = classify_error_kind(error);
|
||||||
return resolve_model_alias_with_config(&config_model);
|
let (short_reason, inline_hint) = split_error_hint(error);
|
||||||
}
|
let hint = inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
|
||||||
cli_model
|
let mut value = status_json_value(None, usage, permission_mode, context, None, allowed_tools);
|
||||||
|
let object = value
|
||||||
|
.as_object_mut()
|
||||||
|
.expect("status_json_value should render an object");
|
||||||
|
object.insert("status".to_string(), serde_json::json!("warn"));
|
||||||
|
object.insert("error_kind".to_string(), serde_json::json!(kind));
|
||||||
|
object.insert(
|
||||||
|
"model_validation_error".to_string(),
|
||||||
|
serde_json::json!(short_reason),
|
||||||
|
);
|
||||||
|
object.insert(
|
||||||
|
"model_validation_error_kind".to_string(),
|
||||||
|
serde_json::json!(kind),
|
||||||
|
);
|
||||||
|
object.insert("model_validation_hint".to_string(), serde_json::json!(hint));
|
||||||
|
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn provider_label(kind: ProviderKind) -> &'static str {
|
fn provider_label(kind: ProviderKind) -> &'static str {
|
||||||
@@ -5199,7 +5268,7 @@ fn run_repl(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
|
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
|
||||||
run_stale_base_preflight(base_commit.as_deref());
|
run_stale_base_preflight(base_commit.as_deref());
|
||||||
let resolved_model = resolve_repl_model(model);
|
let resolved_model = resolve_repl_model(model)?;
|
||||||
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
||||||
cli.set_reasoning_effort(reasoning_effort);
|
cli.set_reasoning_effort(reasoning_effort);
|
||||||
let mut editor =
|
let mut editor =
|
||||||
@@ -7567,14 +7636,25 @@ fn print_status_snapshot(
|
|||||||
// #148: resolve model provenance. If user passed --model, source is
|
// #148: resolve model provenance. If user passed --model, source is
|
||||||
// "flag" with the raw input preserved. Otherwise probe env -> config
|
// "flag" with the raw input preserved. Otherwise probe env -> config
|
||||||
// -> default and record the winning source.
|
// -> default and record the winning source.
|
||||||
let provenance = match model_flag_raw {
|
let provenance_result = match model_flag_raw {
|
||||||
Some(raw) => ModelProvenance {
|
Some(raw) => Ok(ModelProvenance::from_flag(raw, model)),
|
||||||
resolved: model.to_string(),
|
|
||||||
raw: Some(raw.to_string()),
|
|
||||||
source: ModelSource::Flag,
|
|
||||||
},
|
|
||||||
None => ModelProvenance::from_env_or_config_or_default(model),
|
None => ModelProvenance::from_env_or_config_or_default(model),
|
||||||
};
|
};
|
||||||
|
let provenance = match provenance_result {
|
||||||
|
Ok(provenance) => provenance,
|
||||||
|
Err(error) => match output_format {
|
||||||
|
CliOutputFormat::Json => {
|
||||||
|
return print_model_validation_warning_status(
|
||||||
|
&error,
|
||||||
|
usage,
|
||||||
|
permission_mode.as_str(),
|
||||||
|
&context,
|
||||||
|
allowed_tools,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CliOutputFormat::Text => return Err(error.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
match output_format {
|
match output_format {
|
||||||
CliOutputFormat::Text => println!(
|
CliOutputFormat::Text => println!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -7624,6 +7704,8 @@ fn status_json_value(
|
|||||||
let degraded = context.config_load_error.is_some();
|
let degraded = context.config_load_error.is_some();
|
||||||
let model_source = provenance.map(|p| p.source.as_str());
|
let model_source = provenance.map(|p| p.source.as_str());
|
||||||
let model_raw = provenance.and_then(|p| p.raw.clone());
|
let model_raw = provenance.and_then(|p| p.raw.clone());
|
||||||
|
let model_alias_resolved_to = provenance.and_then(|p| p.alias_resolved_to.clone());
|
||||||
|
let model_env_var = provenance.and_then(|p| p.env_var.clone());
|
||||||
// #732: always emit an array (empty when unrestricted) so callers can do
|
// #732: always emit an array (empty when unrestricted) so callers can do
|
||||||
// `.allowed_tools.entries | length > 0` without a null-check first.
|
// `.allowed_tools.entries | length > 0` without a null-check first.
|
||||||
let allowed_tool_entries = allowed_tools
|
let allowed_tool_entries = allowed_tools
|
||||||
@@ -7638,6 +7720,8 @@ fn status_json_value(
|
|||||||
"model": model,
|
"model": model,
|
||||||
"model_source": model_source,
|
"model_source": model_source,
|
||||||
"model_raw": model_raw,
|
"model_raw": model_raw,
|
||||||
|
"model_alias_resolved_to": model_alias_resolved_to,
|
||||||
|
"model_env_var": model_env_var,
|
||||||
"permission_mode": permission_mode,
|
"permission_mode": permission_mode,
|
||||||
"allowed_tools": {
|
"allowed_tools": {
|
||||||
"source": if allowed_tools.is_some() { "flag" } else { "default" },
|
"source": if allowed_tools.is_some() { "flag" } else { "default" },
|
||||||
@@ -7808,9 +7892,22 @@ fn format_status_report(
|
|||||||
let model_source_line = provenance
|
let model_source_line = provenance
|
||||||
.map(|p| match &p.raw {
|
.map(|p| match &p.raw {
|
||||||
Some(raw) if raw != model => {
|
Some(raw) if raw != model => {
|
||||||
format!("\n Model source {} (raw: {raw})", p.source.as_str())
|
let env_suffix = p
|
||||||
|
.env_var
|
||||||
|
.as_deref()
|
||||||
|
.map_or(String::new(), |name| format!(" via {name}"));
|
||||||
|
format!(
|
||||||
|
"\n Model source {}{env_suffix} (raw: {raw}, alias: {model})",
|
||||||
|
p.source.as_str()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
let env_suffix = p
|
||||||
|
.env_var
|
||||||
|
.as_deref()
|
||||||
|
.map_or(String::new(), |name| format!(" via {name}"));
|
||||||
|
format!("\n Model source {}{env_suffix}", p.source.as_str())
|
||||||
}
|
}
|
||||||
Some(_) => format!("\n Model source {}", p.source.as_str()),
|
|
||||||
None => format!("\n Model source {}", p.source.as_str()),
|
None => format!("\n Model source {}", p.source.as_str()),
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@@ -12423,7 +12520,7 @@ mod tests {
|
|||||||
parse_args(&args).expect("args should parse"),
|
parse_args(&args).expect("args should parse"),
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt: "explain this".to_string(),
|
prompt: "explain this".to_string(),
|
||||||
model: "anthropic/claude-opus-4-6".to_string(),
|
model: "anthropic/claude-opus-4-7".to_string(),
|
||||||
output_format: CliOutputFormat::Json,
|
output_format: CliOutputFormat::Json,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
@@ -12497,7 +12594,7 @@ mod tests {
|
|||||||
parse_args(&args).expect("args should parse"),
|
parse_args(&args).expect("args should parse"),
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt: "explain this".to_string(),
|
prompt: "explain this".to_string(),
|
||||||
model: "anthropic/claude-opus-4-6".to_string(),
|
model: "anthropic/claude-opus-4-7".to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
@@ -12511,7 +12608,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_known_model_aliases() {
|
fn resolves_known_model_aliases() {
|
||||||
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-6");
|
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-7");
|
||||||
assert_eq!(resolve_model_alias("sonnet"), "anthropic/claude-sonnet-4-6");
|
assert_eq!(resolve_model_alias("sonnet"), "anthropic/claude-sonnet-4-6");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_model_alias("haiku"),
|
resolve_model_alias("haiku"),
|
||||||
@@ -12522,8 +12619,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_model_alias_uses_anthropic_routing_prefix() {
|
fn default_model_alias_uses_anthropic_routing_prefix() {
|
||||||
assert_eq!(DEFAULT_MODEL, "anthropic/claude-opus-4-6");
|
assert_eq!(DEFAULT_MODEL, "anthropic/claude-opus-4-7");
|
||||||
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-6");
|
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-7");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -12559,7 +12656,7 @@ mod tests {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(direct, "anthropic/claude-haiku-4-5-20251213");
|
assert_eq!(direct, "anthropic/claude-haiku-4-5-20251213");
|
||||||
assert_eq!(chained, "anthropic/claude-opus-4-6");
|
assert_eq!(chained, "anthropic/claude-opus-4-7");
|
||||||
assert_eq!(cross_provider, "grok-3-mini");
|
assert_eq!(cross_provider, "grok-3-mini");
|
||||||
assert_eq!(unknown, "unknown-model");
|
assert_eq!(unknown, "unknown-model");
|
||||||
assert_eq!(builtin, "anthropic/claude-haiku-4-5-20251213");
|
assert_eq!(builtin, "anthropic/claude-haiku-4-5-20251213");
|
||||||
@@ -14209,7 +14306,7 @@ mod tests {
|
|||||||
.expect("prompt shorthand should still work"),
|
.expect("prompt shorthand should still work"),
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt: "please debug this".to_string(),
|
prompt: "please debug this".to_string(),
|
||||||
model: "anthropic/claude-opus-4-6".to_string(),
|
model: "anthropic/claude-opus-4-7".to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: crate::default_permission_mode(),
|
permission_mode: crate::default_permission_mode(),
|
||||||
@@ -14793,7 +14890,7 @@ mod tests {
|
|||||||
fn resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit() {
|
fn resolve_repl_model_returns_user_supplied_model_unchanged_when_explicit() {
|
||||||
let user_model = "anthropic/claude-sonnet-4-6".to_string();
|
let user_model = "anthropic/claude-sonnet-4-6".to_string();
|
||||||
|
|
||||||
let resolved = resolve_repl_model(user_model);
|
let resolved = resolve_repl_model(user_model).expect("explicit model should resolve");
|
||||||
|
|
||||||
assert_eq!(resolved, "anthropic/claude-sonnet-4-6");
|
assert_eq!(resolved, "anthropic/claude-sonnet-4-6");
|
||||||
}
|
}
|
||||||
@@ -14809,7 +14906,8 @@ mod tests {
|
|||||||
std::env::remove_var("ANTHROPIC_MODEL");
|
std::env::remove_var("ANTHROPIC_MODEL");
|
||||||
std::env::set_var("ANTHROPIC_MODEL", "sonnet");
|
std::env::set_var("ANTHROPIC_MODEL", "sonnet");
|
||||||
|
|
||||||
let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()))
|
||||||
|
.expect("env model should resolve");
|
||||||
|
|
||||||
assert_eq!(resolved, "anthropic/claude-sonnet-4-6");
|
assert_eq!(resolved, "anthropic/claude-sonnet-4-6");
|
||||||
|
|
||||||
@@ -14828,7 +14926,8 @@ mod tests {
|
|||||||
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
std::env::remove_var("ANTHROPIC_MODEL");
|
std::env::remove_var("ANTHROPIC_MODEL");
|
||||||
|
|
||||||
let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()))
|
||||||
|
.expect("default model should resolve");
|
||||||
|
|
||||||
assert_eq!(resolved, DEFAULT_MODEL);
|
assert_eq!(resolved, DEFAULT_MODEL);
|
||||||
|
|
||||||
@@ -17020,7 +17119,7 @@ mod alias_resolution_tests {
|
|||||||
// Built-in aliases should resolve to their full IDs
|
// Built-in aliases should resolve to their full IDs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_model_alias_with_config("opus"),
|
resolve_model_alias_with_config("opus"),
|
||||||
"anthropic/claude-opus-4-6"
|
"anthropic/claude-opus-4-7"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_model_alias_with_config("sonnet"),
|
resolve_model_alias_with_config("sonnet"),
|
||||||
|
|||||||
@@ -307,6 +307,84 @@ fn status_json_surfaces_permission_mode_override_for_security_audit() {
|
|||||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_json_accepts_namespaced_model_env_and_surfaces_alias_426() {
|
||||||
|
let root = unique_temp_dir("status-model-env-426");
|
||||||
|
let config_home = root.join("config-home");
|
||||||
|
let home = root.join("home");
|
||||||
|
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||||
|
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||||
|
fs::create_dir_all(&home).expect("home should exist");
|
||||||
|
|
||||||
|
let envs = [
|
||||||
|
(
|
||||||
|
"CLAW_CONFIG_HOME",
|
||||||
|
config_home.to_str().expect("utf8 config home"),
|
||||||
|
),
|
||||||
|
("HOME", home.to_str().expect("utf8 home")),
|
||||||
|
("CLAW_MODEL", "opus"),
|
||||||
|
("ANTHROPIC_MODEL", ""),
|
||||||
|
("ANTHROPIC_DEFAULT_MODEL", ""),
|
||||||
|
];
|
||||||
|
let parsed = assert_json_command_with_env(&root, &["--output-format", "json", "status"], &envs);
|
||||||
|
|
||||||
|
assert_eq!(parsed["status"], "ok");
|
||||||
|
assert_eq!(parsed["model"], "anthropic/claude-opus-4-7");
|
||||||
|
assert_eq!(parsed["model_source"], "env");
|
||||||
|
assert_eq!(parsed["model_raw"], "opus");
|
||||||
|
assert_eq!(
|
||||||
|
parsed["model_alias_resolved_to"],
|
||||||
|
"anthropic/claude-opus-4-7"
|
||||||
|
);
|
||||||
|
assert_eq!(parsed["model_env_var"], "CLAW_MODEL");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_json_warns_on_invalid_model_env_426() {
|
||||||
|
let root = unique_temp_dir("status-invalid-model-env-426");
|
||||||
|
let config_home = root.join("config-home");
|
||||||
|
let home = root.join("home");
|
||||||
|
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||||
|
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||||
|
fs::create_dir_all(&home).expect("home should exist");
|
||||||
|
|
||||||
|
let envs = [
|
||||||
|
(
|
||||||
|
"CLAW_CONFIG_HOME",
|
||||||
|
config_home.to_str().expect("utf8 config home"),
|
||||||
|
),
|
||||||
|
("HOME", home.to_str().expect("utf8 home")),
|
||||||
|
("CLAW_MODEL", ""),
|
||||||
|
("ANTHROPIC_MODEL", "bogus-model-xyz"),
|
||||||
|
("ANTHROPIC_DEFAULT_MODEL", ""),
|
||||||
|
];
|
||||||
|
let output = run_claw(&root, &["--output-format", "json", "status"], &envs);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"invalid env model should produce status warn, not process abort; stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let parsed: Value = serde_json::from_slice(&output.stdout).expect("stdout valid json");
|
||||||
|
|
||||||
|
assert_eq!(parsed["kind"], "status");
|
||||||
|
assert_eq!(parsed["status"], "warn");
|
||||||
|
assert_eq!(parsed["model"], Value::Null);
|
||||||
|
assert_eq!(parsed["model_validation_error_kind"], "invalid_model");
|
||||||
|
assert_eq!(parsed["error_kind"], "invalid_model");
|
||||||
|
assert!(
|
||||||
|
parsed["model_validation_error"]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|message| message.contains("ANTHROPIC_MODEL")
|
||||||
|
&& message.contains("bogus-model-xyz")),
|
||||||
|
"warning should name env var and raw model: {parsed}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
parsed["workspace"].is_object(),
|
||||||
|
"status warning should keep local context: {parsed}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn acp_guidance_emits_json_when_requested() {
|
fn acp_guidance_emits_json_when_requested() {
|
||||||
let root = unique_temp_dir("acp-json");
|
let root = unique_temp_dir("acp-json");
|
||||||
|
|||||||
Reference in New Issue
Block a user