opencode prices and Go limits, settings out of .env
- Fetch prices for opencode (Zen and Go) incl. limits, image input, reasoning,
long-context tiers and peak-hour/date factors; costs apply them per request
- Record the cost opencode reports per response; track Go allowance in 5h/7d/month
windows against a provider plan limit (budget table, status overlay)
- Settings → Config file: move values of ${NAME} references into the config
- Examples, presets and forms keep keys in the config instead of .env
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70367211c1
commit
bfa1f641d7
23 files changed
+723
-69
No files matched your search
+72
-1
@@ -470,6 +470,57 @@ pub fn load_or_init(path: &Path) -> Result<(Config, String)> {
|
||||
Ok((cfg, text))
|
||||
}
|
||||
|
||||
/// Replace `${NAME}` references in all string values with the environment's values,
|
||||
/// so the settings live in the config only. Comments and formatting stay; references
|
||||
/// to unset variables are kept. Returns (new text, replaced names, unset names).
|
||||
pub fn inline_env(text: &str) -> Result<(String, Vec<String>, Vec<String>)> {
|
||||
use std::collections::BTreeSet;
|
||||
let mut doc: toml_edit::DocumentMut = text.parse().context("config is not valid TOML")?;
|
||||
let re = regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").unwrap();
|
||||
let (mut done, mut missing) = (BTreeSet::new(), BTreeSet::new());
|
||||
let mut fix = |s: &str| -> Option<String> {
|
||||
if !re.is_match(s) {
|
||||
return None;
|
||||
}
|
||||
let out = re.replace_all(s, |c: ®ex::Captures| match std::env::var(&c[1]).ok().filter(|v| !v.is_empty()) {
|
||||
Some(v) => {
|
||||
done.insert(c[1].to_string());
|
||||
v
|
||||
}
|
||||
None => {
|
||||
missing.insert(c[1].to_string());
|
||||
c[0].to_string()
|
||||
}
|
||||
});
|
||||
(out != s).then(|| out.into_owned())
|
||||
};
|
||||
fn walk_value(v: &mut toml_edit::Value, fix: &mut dyn FnMut(&str) -> Option<String>) {
|
||||
match v {
|
||||
toml_edit::Value::String(f) => {
|
||||
if let Some(new) = fix(f.value()) {
|
||||
let decor = f.decor().clone();
|
||||
let mut nv = toml_edit::Formatted::new(new);
|
||||
*nv.decor_mut() = decor;
|
||||
*f = nv;
|
||||
}
|
||||
}
|
||||
toml_edit::Value::Array(a) => a.iter_mut().for_each(|x| walk_value(x, fix)),
|
||||
toml_edit::Value::InlineTable(t) => t.iter_mut().for_each(|(_, x)| walk_value(x, fix)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn walk_item(i: &mut toml_edit::Item, fix: &mut dyn FnMut(&str) -> Option<String>) {
|
||||
match i {
|
||||
toml_edit::Item::Value(v) => walk_value(v, fix),
|
||||
toml_edit::Item::Table(t) => t.iter_mut().for_each(|(_, x)| walk_item(x, fix)),
|
||||
toml_edit::Item::ArrayOfTables(a) => a.iter_mut().for_each(|t| t.iter_mut().for_each(|(_, x)| walk_item(x, fix))),
|
||||
toml_edit::Item::None => {}
|
||||
}
|
||||
}
|
||||
walk_item(doc.as_item_mut(), &mut fix);
|
||||
Ok((doc.to_string(), done.into_iter().collect(), missing.into_iter().collect()))
|
||||
}
|
||||
|
||||
/// A structured edit coming from the UI. Applied with toml_edit so comments survive.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigPatch {
|
||||
@@ -616,6 +667,26 @@ pub fn redacted_json(cfg: &Config) -> serde_json::Value {
|
||||
v
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod inline_env_tests {
|
||||
#[test]
|
||||
fn replaces_set_variables_and_keeps_comments() {
|
||||
// SAFETY: test-only, the variable names are unique to this test.
|
||||
unsafe {
|
||||
std::env::set_var("CC_TEST_INLINE_KEY", "sk-123");
|
||||
std::env::remove_var("CC_TEST_INLINE_UNSET");
|
||||
}
|
||||
let text = "# keys: use ${NAME}\n[providers.a]\napi_key = \"${CC_TEST_INLINE_KEY}\" # stored\nheaders = { Authorization = \"Bearer ${CC_TEST_INLINE_KEY}\" }\nother = \"${CC_TEST_INLINE_UNSET}\"\n";
|
||||
let (out, done, missing) = super::inline_env(text).unwrap();
|
||||
assert!(out.contains("api_key = \"sk-123\" # stored"), "{out}");
|
||||
assert!(out.contains("Bearer sk-123"));
|
||||
assert!(out.contains("# keys: use ${NAME}"), "comments untouched");
|
||||
assert!(out.contains("${CC_TEST_INLINE_UNSET}"));
|
||||
assert_eq!(done, vec!["CC_TEST_INLINE_KEY"]);
|
||||
assert_eq!(missing, vec!["CC_TEST_INLINE_UNSET"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -639,7 +710,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(out.contains("# Google Gemini through its OpenAI-compatible endpoint."));
|
||||
assert!(out.contains("enabled = true # set to true once GEMINI_API_KEY is available"));
|
||||
assert!(out.contains("enabled = true # enable once the key is set"));
|
||||
let cfg = Config::parse(&out).unwrap();
|
||||
assert!(cfg.providers["gemini"].enabled);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user