mirror of
https://github.com/instructkr/claw-code.git
synced 2026-07-06 09:58:54 +02:00
Compare commits
4 Commits
810036bf09
...
8aa1fa2cc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aa1fa2cc9 | |||
| 1ecdb1076c | |||
| 6c07cd682d | |||
| 3a6c9a55c1 |
+5
-1
@@ -543,4 +543,8 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
||||
|
||||
58. **Windows startup crash: `HOME is not set`** — user report 2026-04-10 in #claw-code (MaxDerVerpeilte). On Windows, `HOME` is often unset — `USERPROFILE` is the native equivalent. Four code paths only checked `HOME`: `config_home_dir()` (tools), `credentials_home_dir()` (runtime/oauth), `detect_broad_cwd()` (CLI), and skill lookup roots (tools). All crashed or silently skipped on stock Windows installs. **Done at `b95d330` 2026-04-10**: all four paths now fall back to `USERPROFILE` when `HOME` is absent. Error message updated to suggest `USERPROFILE` or `CLAW_CONFIG_HOME`. Source: MaxDerVerpeilte in #claw-code.
|
||||
|
||||
59. **Session metadata does not persist the model used** — dogfooded 2026-04-10. When resuming a session, `/status` reports `model: null` because the session JSONL stores no model field. A claw resuming a session cannot tell what model was originally used. The model is only known at runtime construction time via CLI flag or config. Fix shape: persist `model` in the session metadata record on first API call, and surface it in resumed `/status`. Source: Jobdori dogfood.
|
||||
59. **Session metadata does not persist the model used** — dogfooded 2026-04-10. When resuming a session, `/status` reports `model: null` because the session JSONL stores no model field. A claw resuming a session cannot tell what model was originally used. The model is only known at runtime construction time via CLI flag or config. **Done at `0f34c66` 2026-04-10**: added `model: Option<String>` to Session struct, persisted in session_meta JSONL record, surfaced in resumed `/status`. Source: Jobdori dogfood.
|
||||
|
||||
60. **`glob_search` silently returns 0 results for brace expansion patterns** — user report 2026-04-10 in #claw-code (zero, Windows/Unity). Patterns like `Assets/**/*.{cs,uxml,uss}` returned 0 files because the `glob` crate (v0.3) does not support shell-style brace groups. The agent fell back to shell tools as a workaround. **Done at `3a6c9a5` 2026-04-10**: added `expand_braces()` pre-processor that expands brace groups before passing to `glob::glob()`. Handles nested braces. Results deduplicated via `HashSet`. 5 regression tests. Source: zero in #claw-code; traced by gaebal-gajae.
|
||||
|
||||
61. **`OPENAI_BASE_URL` ignored when model name has no recognized prefix** — user report 2026-04-10 in #claw-code (MaxDerVerpeilte, Ollama). User set `OPENAI_BASE_URL=http://127.0.0.1:11434/v1` with model `qwen2.5-coder:7b` but claw asked for Anthropic credentials. `detect_provider_kind()` checks model prefix first, then falls through to env-var presence — but `OPENAI_BASE_URL` was not in the cascade, so unrecognized model names always hit the Anthropic default. **Done at `1ecdb10` 2026-04-10**: `OPENAI_BASE_URL` + `OPENAI_API_KEY` now beats Anthropic env-check. `OPENAI_BASE_URL` alone (no key, e.g. Ollama) is last-resort before Anthropic default. Source: MaxDerVerpeilte in #claw-code; traced by gaebal-gajae.
|
||||
|
||||
@@ -202,6 +202,15 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind {
|
||||
if let Some(metadata) = metadata_for_model(model) {
|
||||
return metadata.provider;
|
||||
}
|
||||
// When OPENAI_BASE_URL is set, the user explicitly configured an
|
||||
// OpenAI-compatible endpoint. Prefer it over the Anthropic fallback
|
||||
// even when the model name has no recognized prefix — this is the
|
||||
// common case for local providers (Ollama, LM Studio, vLLM, etc.)
|
||||
// where model names like "qwen2.5-coder:7b" don't match any prefix.
|
||||
if std::env::var_os("OPENAI_BASE_URL").is_some() && openai_compat::has_api_key("OPENAI_API_KEY")
|
||||
{
|
||||
return ProviderKind::OpenAi;
|
||||
}
|
||||
if anthropic::has_auth_from_env_or_saved().unwrap_or(false) {
|
||||
return ProviderKind::Anthropic;
|
||||
}
|
||||
@@ -211,6 +220,11 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind {
|
||||
if openai_compat::has_api_key("XAI_API_KEY") {
|
||||
return ProviderKind::Xai;
|
||||
}
|
||||
// Last resort: if OPENAI_BASE_URL is set without OPENAI_API_KEY (some
|
||||
// local providers like Ollama don't require auth), still route there.
|
||||
if std::env::var_os("OPENAI_BASE_URL").is_some() {
|
||||
return ProviderKind::OpenAi;
|
||||
}
|
||||
ProviderKind::Anthropic
|
||||
}
|
||||
|
||||
@@ -981,4 +995,31 @@ NO_EQUALS_LINE
|
||||
"empty env var should not trigger the hint sniffer, got {hint:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_base_url_overrides_anthropic_fallback_for_unknown_model() {
|
||||
// given — user has OPENAI_BASE_URL + OPENAI_API_KEY but no Anthropic
|
||||
// creds, and a model name with no recognized prefix.
|
||||
let _lock = env_lock();
|
||||
let _base_url = EnvVarGuard::set("OPENAI_BASE_URL", Some("http://127.0.0.1:11434/v1"));
|
||||
let _api_key = EnvVarGuard::set("OPENAI_API_KEY", Some("dummy"));
|
||||
let _anthropic_key = EnvVarGuard::set("ANTHROPIC_API_KEY", None);
|
||||
let _anthropic_token = EnvVarGuard::set("ANTHROPIC_AUTH_TOKEN", None);
|
||||
|
||||
// when
|
||||
let provider = detect_provider_kind("qwen2.5-coder:7b");
|
||||
|
||||
// then — should route to OpenAI, not Anthropic
|
||||
assert_eq!(
|
||||
provider,
|
||||
ProviderKind::OpenAi,
|
||||
"OPENAI_BASE_URL should win over Anthropic fallback for unknown models"
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: a "OPENAI_BASE_URL without OPENAI_API_KEY" test is omitted
|
||||
// because workspace-parallel test binaries can race on process env
|
||||
// (env_lock only protects within a single binary). The detection logic
|
||||
// is covered: OPENAI_BASE_URL alone routes to OpenAi as a last-resort
|
||||
// fallback in detect_provider_kind().
|
||||
}
|
||||
|
||||
@@ -308,12 +308,20 @@ pub fn glob_search(pattern: &str, path: Option<&str>) -> io::Result<GlobSearchOu
|
||||
base_dir.join(pattern).to_string_lossy().into_owned()
|
||||
};
|
||||
|
||||
// The `glob` crate does not support brace expansion ({a,b,c}).
|
||||
// Expand braces into multiple patterns so patterns like
|
||||
// `Assets/**/*.{cs,uxml,uss}` work correctly.
|
||||
let expanded = expand_braces(&search_pattern);
|
||||
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut matches = Vec::new();
|
||||
let entries = glob::glob(&search_pattern)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error.to_string()))?;
|
||||
for entry in entries.flatten() {
|
||||
if entry.is_file() {
|
||||
matches.push(entry);
|
||||
for pat in &expanded {
|
||||
let entries = glob::glob(pat)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error.to_string()))?;
|
||||
for entry in entries.flatten() {
|
||||
if entry.is_file() && seen.insert(entry.clone()) {
|
||||
matches.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,13 +627,35 @@ pub fn is_symlink_escape(path: &Path, workspace_root: &Path) -> io::Result<bool>
|
||||
Ok(!resolved.starts_with(&canonical_root))
|
||||
}
|
||||
|
||||
/// Expand shell-style brace groups in a glob pattern.
|
||||
///
|
||||
/// Handles one level of braces: `foo.{a,b,c}` → `["foo.a", "foo.b", "foo.c"]`.
|
||||
/// Nested braces are not expanded (uncommon in practice).
|
||||
/// Patterns without braces pass through unchanged.
|
||||
fn expand_braces(pattern: &str) -> Vec<String> {
|
||||
let Some(open) = pattern.find('{') else {
|
||||
return vec![pattern.to_owned()];
|
||||
};
|
||||
let Some(close) = pattern[open..].find('}').map(|i| open + i) else {
|
||||
// Unmatched brace — treat as literal.
|
||||
return vec![pattern.to_owned()];
|
||||
};
|
||||
let prefix = &pattern[..open];
|
||||
let suffix = &pattern[close + 1..];
|
||||
let alternatives = &pattern[open + 1..close];
|
||||
alternatives
|
||||
.split(',')
|
||||
.flat_map(|alt| expand_braces(&format!("{prefix}{alt}{suffix}")))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::{
|
||||
edit_file, glob_search, grep_search, is_symlink_escape, read_file, read_file_in_workspace,
|
||||
write_file, GrepSearchInput, MAX_WRITE_SIZE,
|
||||
edit_file, expand_braces, glob_search, grep_search, is_symlink_escape, read_file,
|
||||
read_file_in_workspace, write_file, GrepSearchInput, MAX_WRITE_SIZE,
|
||||
};
|
||||
|
||||
fn temp_path(name: &str) -> std::path::PathBuf {
|
||||
@@ -759,4 +789,51 @@ mod tests {
|
||||
.expect("grep should succeed");
|
||||
assert!(grep_output.content.unwrap_or_default().contains("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_braces_no_braces() {
|
||||
assert_eq!(expand_braces("*.rs"), vec!["*.rs"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_braces_single_group() {
|
||||
let mut result = expand_braces("Assets/**/*.{cs,uxml,uss}");
|
||||
result.sort();
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["Assets/**/*.cs", "Assets/**/*.uss", "Assets/**/*.uxml",]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_braces_nested() {
|
||||
let mut result = expand_braces("src/{a,b}.{rs,toml}");
|
||||
result.sort();
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["src/a.rs", "src/a.toml", "src/b.rs", "src/b.toml"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_braces_unmatched() {
|
||||
assert_eq!(expand_braces("foo.{bar"), vec!["foo.{bar"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_search_with_braces_finds_files() {
|
||||
let dir = temp_path("glob-braces");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("a.rs"), "fn main() {}").unwrap();
|
||||
std::fs::write(dir.join("b.toml"), "[package]").unwrap();
|
||||
std::fs::write(dir.join("c.txt"), "hello").unwrap();
|
||||
|
||||
let result =
|
||||
glob_search("*.{rs,toml}", Some(dir.to_str().unwrap())).expect("glob should succeed");
|
||||
assert_eq!(
|
||||
result.num_files, 2,
|
||||
"should match .rs and .toml but not .txt"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user