Files

1 line
26 KiB
JSON

{"repo": "abdk4moura/claude-go", "files": {"src/models.rs": {"efaa850b800855f9682212e6ef2e01faa737df64": {"t": 1785846936.6322086, "n": 4229, "h": "56aeabdbb76a2ddf", "c": "use std::sync::Mutex;\nuse std::time::{Duration, Instant};\n\nuse once_cell::sync::Lazy;\n\nuse crate::error::ClaudeGoError;\nuse crate::provider::{Model, ModelSource};\n\n/// How long the live model-list response is cached in-process.\n/// 5 minutes, matching the spec.\nconst LIVE_CACHE_TTL: Duration = Duration::from_secs(5 * 60);\n/// Timeout for the live fetch. 5s, fail-fast.\nconst LIVE_FETCH_TIMEOUT: Duration = Duration::from_secs(5);\n\n/// Process-wide cache of the OpenCode Go live model list. Storing the\n/// `Option<Vec<Model>>` lets us cache both the \"fetched successfully\"\n/// case and the \"fetch failed, using fallback\" case without re-trying\n/// on every screen render.\nstatic OPENCODE_GO_CACHE: Lazy<Mutex<Option<CacheEntry>>> = Lazy::new(|| Mutex::new(None));\n\nstruct CacheEntry {\n fetched_at: Instant,\n models: Vec<Model>,\n /// True if the live fetch succeeded; false if we used the fallback\n /// because the network call failed.\n from_live: bool,\n}\n\n/// Return the OpenCode Go model list. Tries the live endpoint first;\n/// on failure (or cache hit), returns the cached or fallback list.\npub async fn opencode_go_models() -> (Vec<Model>, bool) {\n // Cache hit?\n if let Some(entry) = OPENCODE_GO_CACHE.lock().unwrap().as_ref() {\n if entry.fetched_at.elapsed() < LIVE_CACHE_TTL {\n return (entry.models.clone(), entry.from_live);\n }\n }\n\n let url = \"https://opencode.ai/zen/go/v1/models\";\n let client = match reqwest::Client::builder()\n .timeout(LIVE_FETCH_TIMEOUT)\n .build()\n {\n Ok(c) => c,\n Err(_) => return (fallback(), false),\n };\n\n let resp = client.get(url).send().await;\n let models = match resp {\n Ok(r) if r.status().is_success() => match r.json::<OpenCodeGoList>().await {\n Ok(list) => list\n .data\n .into_iter()\n .map(|m| {\n let id = m.id.clone();\n Model::new(m.id, format!(\"live: {id}\"))\n })\n .collect(),\n Err(_) => fallback(),\n },\n _ => fallback(),\n };\n let from_live = !models.is_empty() && models[0].description.starts_with(\"live: \");\n\n *OPENCODE_GO_CACHE.lock().unwrap() = Some(CacheEntry {\n fetched_at: Instant::now(),\n models: models.clone(),\n from_live,\n });\n\n (models, from_live)\n}\n\n#[derive(serde::Deserialize)]\nstruct OpenCodeGoList {\n data: Vec<OpenCodeGoEntry>,\n}\n\n#[derive(serde::Deserialize)]\nstruct OpenCodeGoEntry {\n id: String,\n}\n\n/// Synchronous fallback for contexts where we can't await (e.g. the\n/// `models` CLI subcommand). Returns the hardcoded 19-model list.\npub fn fallback() -> Vec<Model> {\n // Built-in OpenCode Go provider is at index 0 in `built_in_presets()`.\n let preset = &crate::provider::built_in_presets()[0];\n match &preset.model_source {\n ModelSource::Live { fallback, .. } => fallback.clone(),\n _ => unreachable!(\"opencode-go preset must have a Live model source\"),\n }\n}\n\n/// Validate a model id against a provider's model source. For `Any`,\n/// anything non-empty is acceptable. For `Fixed`, the id must be in\n/// the list. For `Live`, the id must be in either the live cache or\n/// the fallback.\npub fn validate_model(source: &ModelSource, id: &str) -> Result<(), ClaudeGoError> {\n if id.is_empty() {\n return Err(ClaudeGoError::UnknownModel(id.to_string()));\n }\n match source {\n ModelSource::Any => Ok(()),\n ModelSource::Fixed(list) => {\n if list.iter().any(|m| m.id == id) {\n Ok(())\n } else {\n Err(ClaudeGoError::UnknownModel(id.to_string()))\n }\n }\n ModelSource::Live { fallback, .. } => {\n // Best-effort: also accept the live cache.\n if let Some(entry) = OPENCODE_GO_CACHE.lock().unwrap().as_ref() {\n if entry.models.iter().any(|m| m.id == id) {\n return Ok(());\n }\n }\n if fallback.iter().any(|m| m.id == id) {\n Ok(())\n } else {\n Err(ClaudeGoError::UnknownModel(id.to_string()))\n }\n }\n }\n}\n"}}, "src/settings.rs": {"efaa850b800855f9682212e6ef2e01faa737df64": {"t": 1785846938.2327874, "n": 21409, "h": "2c10f8af9816c2f4", "c": "use std::path::Path;\n\nuse serde_json::{Map, Value};\n\nuse crate::error::ClaudeGoError;\nuse crate::paths::Paths;\nuse crate::provider::{Provider, ProviderFormat};\n\n/// The exact set of env keys owned by claude-go. Every write to\n/// `~/.claude/settings.json` sets these (in addition to the marker),\n/// and `turn_off` strips only these (gated on the marker being present).\n///\n/// IMPORTANT: this list must match bash v0.1.1 bit-for-bit. Adding or\n/// removing a key here is a user-facing change to the settings.json\n/// contract that Claude Code reads on every launch.\npub const OWNED_ENV_KEYS: &[&str] = &[\n \"ANTHROPIC_BASE_URL\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"ANTHROPIC_MODEL\",\n \"ANTHROPIC_DEFAULT_HAIKU_MODEL\",\n \"ANTHROPIC_DEFAULT_SONNET_MODEL\",\n \"ANTHROPIC_DEFAULT_OPUS_MODEL\",\n \"DISABLE_TELEMETRY\",\n \"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\",\n];\n\n/// Marker written into the `env` dict. `turn_off` strips the owned keys\n/// iff this marker is present and equals `\"1\"`. Without it, a user with\n/// their own ANTHROPIC_* keys (e.g. pointing at an internal proxy) would\n/// have those keys silently destroyed on `off`.\npub const OWNERSHIP_MARKER: &str = \"__claude_go_owned\";\n\n/// Parsed view of `~/.claude/settings.json`. Trust the inner types once\n/// you have one of these -- the boundary parse enforces the invariants\n/// (env is an object if present, model/base_url/key are strings).\n#[derive(Debug, Clone)]\npub struct SettingsState {\n pub enabled: bool,\n pub base_url: String,\n pub model: String,\n pub path_kind: PathKind,\n pub key_in_settings: bool,\n /// True iff claude-go wrote this env block (marker present). `off`\n /// only strips our own keys when this is true.\n pub owned: bool,\n}\n\n/// The format the current base URL points at. Parsed from the URL\n/// shape, not from a stored string. This is \"parse, don't validate\":\n/// once we have a `PathKind`, we don't re-check the URL.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum PathKind {\n Anthropic,\n OpenAI,\n Other,\n}\n\nimpl PathKind {\n /// Discriminate the path kind from a base URL. CRLF-safe: strips\n /// stray `\\r\\n` defensively to keep peek robust against editor\n /// corruption of the settings file.\n pub fn from_base_url(base_url: &str) -> Self {\n let sanitized = base_url.replace('\\r', \"\").replace('\\n', \"\").trim().to_string();\n if sanitized == \"https://opencode.ai/zen/go\" {\n Self::Anthropic\n } else if sanitized.starts_with(\"http://127.0.0.1:\") {\n Self::OpenAI\n } else {\n Self::Other\n }\n }\n}\n\nimpl SettingsState {\n /// Read `~/.claude/settings.json` and parse it into a `SettingsState`.\n /// Missing file = all-default state (not an error).\n pub fn peek(paths: &Paths) -> Result<Self, ClaudeGoError> {\n let Some(state) = read_raw(&paths.settings_file)? else {\n return Ok(Self::disabled_default());\n };\n Ok(Self::from_value(&state))\n }\n\n /// Convenience: read from default paths.\n pub fn peek_default() -> Result<Self, ClaudeGoError> {\n Self::peek(&Paths::resolve())\n }\n\n /// A default \"disabled\" settings state, used as the fallback when\n /// `peek` fails. Public so other modules (e.g. the TTY fallback in\n /// `tty.rs`) can construct a known-disabled state without having\n /// to fake up a `Paths`.\n pub fn disabled_default() -> Self {\n Self {\n enabled: false,\n base_url: String::new(),\n model: String::new(),\n path_kind: PathKind::Other,\n key_in_settings: false,\n owned: false,\n }\n }\n\n fn from_value(root: &Value) -> Self {\n // A non-object `env` is treated as if env were empty. This is\n // the safe default -- never assume ownership of a non-dict\n // env, so we strip nothing and never claim to be enabled.\n let env = root.get(\"env\").and_then(|v| v.as_object());\n let Some(env) = env else {\n return Self::disabled_default();\n };\n\n let owned = env.get(OWNERSHIP_MARKER).and_then(Value::as_str) == Some(\"1\");\n\n let raw_base = env\n .get(\"ANTHROPIC_BASE_URL\")\n .and_then(Value::as_str)\n .unwrap_or(\"\");\n let base_url = raw_base.replace('\\r', \"\").replace('\\n', \"\").trim().to_string();\n let raw_model = env\n .get(\"ANTHROPIC_MODEL\")\n .and_then(Value::as_str)\n .unwrap_or(\"\");\n let model = raw_model.replace('\\r', \"\").replace('\\n', \"\").trim().to_string();\n let key_in_settings = env.contains_key(\"ANTHROPIC_AUTH_TOKEN\");\n let path_kind = PathKind::from_base_url(&base_url);\n let enabled = owned && !path_kind.eq(&PathKind::Other) && key_in_settings && !model.is_empty();\n\n Self {\n enabled,\n base_url,\n model,\n path_kind,\n key_in_settings,\n owned,\n }\n }\n}\n\n/// Inputs to `turn_on`. Built up by the CLI subcommand before calling.\n#[derive(Debug, Clone)]\npub struct TurnOnInputs<'a> {\n pub provider: &'a Provider,\n pub model: &'a str,\n /// Resolved wire format for the chosen model+provider pair. For\n /// OpenCode Go the format is per-model (some are Anthropic, some\n /// are OpenAI), so the caller must compute it before calling.\n /// For other providers this matches `provider.format`.\n pub format: ProviderFormat,\n /// Local port to point ANTHROPIC_BASE_URL at. Required for\n /// OpenAI-format (where the base URL is\n /// `http://127.0.0.1:<port>`). Ignored for Anthropic-format.\n pub port: Option<u16>,\n /// Auth value. Resolved by the caller (OPENCODE_API_KEY env, or\n /// whatever the user configured for the provider).\n pub auth_token: &'a str,\n}\n\n/// Write the owned env block into `~/.claude/settings.json`. Preserves\n/// any other keys the user has in `env` (e.g. custom env vars,\n/// permissions, theme, plugins -- all on the `root`).\npub fn turn_on(paths: &Paths, input: &TurnOnInputs<'_>) -> Result<(), ClaudeGoError> {\n let base_url = if input.format == ProviderFormat::OpenAI {\n let port = input\n .port\n .ok_or_else(|| ClaudeGoError::InvalidPort(0))?;\n format!(\"http://127.0.0.1:{port}\")\n } else {\n input.provider.base_url.clone()\n };\n\n let mut root = read_raw(&paths.settings_file)?.unwrap_or_else(|| {\n let mut m = Map::new();\n m.insert(\"env\".into(), Value::Object(Map::new()));\n Value::Object(m)\n });\n\n // Refuse to merge into a non-dict env. The user must fix their\n // settings.json by hand.\n let env_value = root\n .get(\"env\")\n .cloned()\n .unwrap_or_else(|| Value::Object(Map::new()));\n let Value::Object(mut env) = env_value else {\n return Err(ClaudeGoError::EnvNotObject {\n found: type_name_of(&env_value).to_string(),\n });\n };\n\n for k in OWNED_ENV_KEYS {\n env.insert(\n (*k).to_string(),\n Value::String(env_value_for(k, &base_url, input.model, input.auth_token)),\n );\n }\n env.insert(OWNERSHIP_MARKER.to_string(), Value::String(\"1\".into()));\n\n // Set the env back. If the user had no env at all, we keep the\n // top-level `env` key present (Claude Code expects an object).\n if let Value::Object(ref mut root_map) = root {\n root_map.insert(\"env\".into(), Value::Object(env));\n } else {\n // Root is not an object -- bail loudly per the bash contract.\n return Err(ClaudeGoError::EnvNotObject {\n found: type_name_of(&root).to_string(),\n });\n }\n\n write_atomic(&paths.settings_file, &root)\n}\n\n/// Strip the owned env block iff the marker is present. If the\n/// settings file does not exist, or the env block lacks the marker,\n/// this is a no-op (idempotent). Never touches user-owned env vars.\npub fn turn_off(paths: &Paths) -> Result<(), ClaudeGoError> {\n let Some(root) = read_raw(&paths.settings_file)? else {\n return Ok(());\n };\n let mut root = root;\n\n let env_value = match root.get(\"env\") {\n Some(v) => v.clone(),\n None => return Ok(()),\n };\n let Value::Object(mut env) = env_value else {\n // Non-dict env: treat as if env were absent. Never claim\n // ownership of a non-dict env.\n return Ok(());\n };\n if env.get(OWNERSHIP_MARKER).and_then(Value::as_str) != Some(\"1\") {\n return Ok(());\n }\n for k in OWNED_ENV_KEYS {\n env.remove(*k);\n }\n env.remove(OWNERSHIP_MARKER);\n\n let Value::Object(ref mut root_map) = root else {\n return Ok(());\n };\n if env.is_empty() {\n root_map.remove(\"env\");\n } else {\n root_map.insert(\"env\".into(), Value::Object(env));\n }\n write_atomic(&paths.settings_file, &root)\n}\n\nfn env_value_for(key: &str, base_url: &str, model: &str, auth: &str) -> String {\n match key {\n \"ANTHROPIC_BASE_URL\" => base_url.to_string(),\n \"ANTHROPIC_AUTH_TOKEN\" => auth.to_string(),\n \"ANTHROPIC_MODEL\"\n | \"ANTHROPIC_DEFAULT_HAIKU_MODEL\"\n | \"ANTHROPIC_DEFAULT_SONNET_MODEL\"\n | \"ANTHROPIC_DEFAULT_OPUS_MODEL\" => model.to_string(),\n \"DISABLE_TELEMETRY\" | \"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\" => \"1\".into(),\n _ => unreachable!(\"OWNED_ENV_KEYS contains unknown key: {key}\"),\n }\n}\n\nfn type_name_of(v: &Value) -> &'static str {\n match v {\n Value::Null => \"null\",\n Value::Bool(_) => \"boolean\",\n Value::Number(_) => \"number\",\n Value::String(_) => \"string\",\n Value::Array(_) => \"array\",\n Value::Object(_) => \"object\",\n }\n}\n\nfn read_raw(path: &Path) -> Result<Option<Value>, ClaudeGoError> {\n if !path.exists() {\n return Ok(None);\n }\n let bytes = std::fs::read(path)?;\n if bytes.is_empty() {\n return Ok(None);\n }\n let v: Value = serde_json::from_slice(&bytes)?;\n Ok(Some(v))\n}\n\nfn write_atomic(path: &Path, value: &Value) -> Result<(), ClaudeGoError> {\n if let Some(parent) = path.parent() {\n if !parent.as_os_str().is_empty() {\n std::fs::create_dir_all(parent)?;\n }\n }\n let parent = path.parent().unwrap_or_else(|| Path::new(\".\"));\n // Temp file MUST be in the same directory so `persist` is a rename,\n // not a cross-filesystem copy (which would not be atomic).\n let tmp = tempfile::NamedTempFile::new_in(parent)?;\n let bytes = serde_json::to_vec_pretty(value)?;\n use std::io::Write;\n let mut f = tmp.as_file();\n f.write_all(&bytes)?;\n f.write_all(b\"\\n\")?;\n f.sync_all()?;\n tmp.persist(path).map_err(|e| e.error)?;\n Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::provider::{ModelSource, Provider, ProviderFormat};\n use std::path::PathBuf;\n\n fn test_paths(dir: &Path) -> Paths {\n Paths::resolve_under(dir)\n }\n\n fn make_provider() -> Provider {\n Provider {\n id: \"test\".into(),\n display_name: \"Test\".into(),\n base_url: \"https://example.test\".into(),\n format: ProviderFormat::Anthropic,\n auth_header: \"x-api-key\".into(),\n model_source: ModelSource::Any,\n implemented: true,\n is_custom: false,\n }\n }\n\n fn fresh_dir(name: &str) -> PathBuf {\n let p = std::env::temp_dir().join(format!(\"claude-go-test-{name}\"));\n let _ = std::fs::remove_dir_all(&p);\n std::fs::create_dir_all(&p).unwrap();\n p\n }\n\n #[test]\n fn owned_marker_and_keys_are_stable() {\n // This is a contract test -- if it ever changes, downstream\n // Claude Code installations will see a different env block.\n // 8 owned env keys + 1 ownership marker = 9 keys total in\n // the env block written by `turn_on`. `ANTHROPIC_API_KEY` is\n // intentionally NOT written: setting both `ANTHROPIC_API_KEY`\n // and `ANTHROPIC_AUTH_TOKEN` to the same value trips a\n // cosmetic startup-banner warning in Claude Code.\n assert_eq!(OWNED_ENV_KEYS.len(), 8);\n assert!(OWNED_ENV_KEYS.contains(&\"ANTHROPIC_BASE_URL\"));\n assert!(OWNED_ENV_KEYS.contains(&\"ANTHROPIC_AUTH_TOKEN\"));\n assert!(!OWNED_ENV_KEYS.contains(&\"ANTHROPIC_API_KEY\"));\n assert!(OWNED_ENV_KEYS.contains(&\"ANTHROPIC_DEFAULT_HAIKU_MODEL\"));\n assert!(OWNED_ENV_KEYS.contains(&\"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\"));\n assert_eq!(OWNERSHIP_MARKER, \"__claude_go_owned\");\n }\n\n #[test]\n fn turn_on_writes_exactly_nine_keys() {\n // The env block written by `turn_on` must contain exactly 9\n // keys: the 8 OWNED_ENV_KEYS plus the OWNERSHIP_MARKER. No\n // extra keys (e.g. ANTHROPIC_API_KEY) -- the bash v0.1.1\n // contract wrote 10, which silenced Claude Code's warning\n // banner.\n let dir = fresh_dir(\"nine-keys\");\n let paths = test_paths(&dir);\n let provider = make_provider();\n\n turn_on(\n &paths,\n &TurnOnInputs {\n provider: &provider,\n model: \"minimax-m3\",\n format: ProviderFormat::Anthropic,\n port: None,\n auth_token: \"sk-test\",\n },\n )\n .unwrap();\n\n let raw = std::fs::read_to_string(&paths.settings_file).unwrap();\n let v: Value = serde_json::from_str(&raw).unwrap();\n let env = v.get(\"env\").and_then(|o| o.as_object()).unwrap();\n\n // Exactly 8 owned keys + 1 marker = 9.\n assert_eq!(env.len(), 9);\n\n // All 8 owned keys are present.\n for k in OWNED_ENV_KEYS {\n assert!(env.contains_key(*k), \"missing owned key: {k}\");\n }\n // Marker is present.\n assert_eq!(env.get(OWNERSHIP_MARKER).unwrap(), \"1\");\n\n // ANTHROPIC_API_KEY is NOT present (we dropped it in v0.2.1).\n assert!(!env.contains_key(\"ANTHROPIC_API_KEY\"));\n\n // Spot-check a few values.\n assert_eq!(env.get(\"ANTHROPIC_AUTH_TOKEN\").unwrap(), \"sk-test\");\n assert_eq!(env.get(\"ANTHROPIC_MODEL\").unwrap(), \"minimax-m3\");\n assert_eq!(env.get(\"DISABLE_TELEMETRY\").unwrap(), \"1\");\n }\n\n #[test]\n fn turn_on_then_off_round_trips() {\n let dir = fresh_dir(\"round-trip\");\n let paths = test_paths(&dir);\n let mut provider = make_provider();\n // Use a base URL that `peek` recognizes as a valid (opencode-go-style)\n // path so the round-trip \"enabled\" check holds.\n provider.base_url = \"https://opencode.ai/zen/go\".into();\n\n turn_on(\n &paths,\n &TurnOnInputs {\n provider: &provider,\n model: \"minimax-m3\",\n format: ProviderFormat::Anthropic,\n port: None,\n auth_token: \"sk-test\",\n },\n )\n .unwrap();\n\n // Read raw file and check the env block is right.\n let raw = std::fs::read_to_string(&paths.settings_file).unwrap();\n let v: Value = serde_json::from_str(&raw).unwrap();\n let env = v.get(\"env\").and_then(|o| o.as_object()).unwrap();\n assert_eq!(env.get(\"ANTHROPIC_BASE_URL\").unwrap(), \"https://opencode.ai/zen/go\");\n assert_eq!(env.get(\"ANTHROPIC_MODEL\").unwrap(), \"minimax-m3\");\n assert_eq!(env.get(\"ANTHROPIC_DEFAULT_HAIKU_MODEL\").unwrap(), \"minimax-m3\");\n assert_eq!(env.get(\"ANTHROPIC_AUTH_TOKEN\").unwrap(), \"sk-test\");\n assert_eq!(env.get(OWNERSHIP_MARKER).unwrap(), \"1\");\n assert_eq!(env.get(\"DISABLE_TELEMETRY\").unwrap(), \"1\");\n\n // State should report enabled.\n let state = SettingsState::peek(&paths).unwrap();\n assert!(state.enabled);\n assert_eq!(state.model, \"minimax-m3\");\n assert_eq!(state.path_kind, PathKind::Anthropic);\n\n // Now turn off.\n turn_off(&paths).unwrap();\n let raw2 = std::fs::read_to_string(&paths.settings_file).unwrap();\n let v2: Value = serde_json::from_str(&raw2).unwrap();\n let env2 = v2.get(\"env\").and_then(|o| o.as_object());\n // env is removed entirely because no other env vars exist.\n assert!(env2.is_none() || env2.unwrap().is_empty());\n\n let state2 = SettingsState::peek(&paths).unwrap();\n assert!(!state2.enabled);\n }\n\n #[test]\n fn turn_off_without_marker_does_not_strip() {\n let dir = fresh_dir(\"no-marker\");\n let paths = test_paths(&dir);\n // Hand-craft a user-owned settings.json with their own ANTHROPIC_*\n // keys, no marker.\n let user_value = serde_json::json!({\n \"env\": {\n \"ANTHROPIC_BASE_URL\": \"https://internal.proxy.corp\",\n \"ANTHROPIC_AUTH_TOKEN\": \"user-key\",\n \"ANTHROPIC_MODEL\": \"claude-internal\"\n }\n });\n std::fs::create_dir_all(paths.settings_file.parent().unwrap()).unwrap();\n std::fs::write(\n &paths.settings_file,\n serde_json::to_vec_pretty(&user_value).unwrap(),\n )\n .unwrap();\n\n turn_off(&paths).unwrap();\n\n let raw = std::fs::read_to_string(&paths.settings_file).unwrap();\n let v: Value = serde_json::from_str(&raw).unwrap();\n let env = v.get(\"env\").and_then(|o| o.as_object()).unwrap();\n // User's keys must still be there.\n assert_eq!(env.get(\"ANTHROPIC_BASE_URL\").unwrap(), \"https://internal.proxy.corp\");\n assert_eq!(env.get(\"ANTHROPIC_AUTH_TOKEN\").unwrap(), \"user-key\");\n assert_eq!(env.get(\"ANTHROPIC_MODEL\").unwrap(), \"claude-internal\");\n }\n\n #[test]\n fn turn_on_preserves_user_env_vars() {\n let dir = fresh_dir(\"preserve\");\n let paths = test_paths(&dir);\n let initial = serde_json::json!({\n \"permissions\": {\"defaultMode\": \"auto\"},\n \"env\": {\n \"MY_CUSTOM_VAR\": \"leave-me-alone\"\n }\n });\n std::fs::create_dir_all(paths.settings_file.parent().unwrap()).unwrap();\n std::fs::write(\n &paths.settings_file,\n serde_json::to_vec_pretty(&initial).unwrap(),\n )\n .unwrap();\n let provider = make_provider();\n turn_on(\n &paths,\n &TurnOnInputs {\n provider: &provider,\n model: \"minimax-m3\",\n format: ProviderFormat::Anthropic,\n port: None,\n auth_token: \"sk-test\",\n },\n )\n .unwrap();\n\n let raw = std::fs::read_to_string(&paths.settings_file).unwrap();\n let v: Value = serde_json::from_str(&raw).unwrap();\n let env = v.get(\"env\").and_then(|o| o.as_object()).unwrap();\n assert_eq!(env.get(\"MY_CUSTOM_VAR\").unwrap(), \"leave-me-alone\");\n assert_eq!(env.get(OWNERSHIP_MARKER).unwrap(), \"1\");\n // top-level keys are still there\n assert!(v.get(\"permissions\").is_some());\n }\n\n #[test]\n fn openai_format_uses_localhost_base_url() {\n let dir = fresh_dir(\"openai-base-url\");\n let paths = test_paths(&dir);\n let mut provider = make_provider();\n provider.format = ProviderFormat::OpenAI;\n provider.base_url = \"https://upstream.invalid\".into();\n\n turn_on(\n &paths,\n &TurnOnInputs {\n provider: &provider,\n model: \"glm-5.2\",\n format: ProviderFormat::OpenAI,\n port: Some(4188),\n auth_token: \"sk-test\",\n },\n )\n .unwrap();\n\n let raw = std::fs::read_to_string(&paths.settings_file).unwrap();\n let v: Value = serde_json::from_str(&raw).unwrap();\n let env = v.get(\"env\").and_then(|o| o.as_object()).unwrap();\n assert_eq!(env.get(\"ANTHROPIC_BASE_URL\").unwrap(), \"http://127.0.0.1:4188\");\n }\n\n #[test]\n fn peek_strips_crlf() {\n // Editor corruption / CRLF safety: a stray \\r in the base URL\n // must not make `peek` claim `enabled = false` when the URL\n // would otherwise be opencode-go.\n let v: Value = serde_json::json!({\n \"env\": {\n \"ANTHROPIC_BASE_URL\": \"https://opencode.ai/zen/go\\r\",\n \"ANTHROPIC_AUTH_TOKEN\": \"sk-x\",\n \"ANTHROPIC_MODEL\": \"minimax-m3\",\n \"__claude_go_owned\": \"1\"\n }\n });\n let state = SettingsState::from_value(&v);\n assert!(state.enabled);\n assert_eq!(state.path_kind, PathKind::Anthropic);\n }\n\n #[test]\n fn env_not_object_errors() {\n let dir = fresh_dir(\"env-not-object\");\n let paths = test_paths(&dir);\n let bad = serde_json::json!({\"env\": \"not-an-object\"});\n std::fs::create_dir_all(paths.settings_file.parent().unwrap()).unwrap();\n std::fs::write(\n &paths.settings_file,\n serde_json::to_vec_pretty(&bad).unwrap(),\n )\n .unwrap();\n let provider = make_provider();\n let err = turn_on(\n &paths,\n &TurnOnInputs {\n provider: &provider,\n model: \"m\",\n format: ProviderFormat::Anthropic,\n port: None,\n auth_token: \"k\",\n },\n )\n .unwrap_err();\n match err {\n ClaudeGoError::EnvNotObject { .. } => {}\n other => panic!(\"unexpected error: {other:?}\"),\n }\n }\n}\n"}}}}