mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-06-02 06:13:46 +02:00
Improve admin UX settings (#471)
## Summary - split the admin UI into Providers, Model Config, and Messaging views - remove generated env, diagnostics, smoke, managed-label, and fixed cloud/runtime settings from the visible admin UX - make Z.ai base URL, Claude workspace, and Claude CLI binary fixed app-level behavior instead of managed env fields ## Verification - uv run ruff format - uv run ruff check - uv run ty check - uv run pytest
This commit is contained in:
@@ -24,7 +24,6 @@ OPENCODE_API_KEY=""
|
|||||||
|
|
||||||
# Z.ai Config (Anthropic-compatible Messages at api.z.ai/api/anthropic)
|
# Z.ai Config (Anthropic-compatible Messages at api.z.ai/api/anthropic)
|
||||||
ZAI_API_KEY=""
|
ZAI_API_KEY=""
|
||||||
ZAI_BASE_URL="https://api.z.ai/api/coding/paas/v4"
|
|
||||||
|
|
||||||
|
|
||||||
# LM Studio Config (local provider, no API key required)
|
# LM Studio Config (local provider, no API key required)
|
||||||
@@ -133,9 +132,7 @@ ALLOWED_DISCORD_CHANNELS=""
|
|||||||
|
|
||||||
|
|
||||||
# Agent Config
|
# Agent Config
|
||||||
CLAUDE_WORKSPACE=
|
|
||||||
ALLOWED_DIR=""
|
ALLOWED_DIR=""
|
||||||
CLAUDE_CLI_BIN="claude"
|
|
||||||
FAST_PREFIX_DETECTION=true
|
FAST_PREFIX_DETECTION=true
|
||||||
ENABLE_NETWORK_PROBE_MOCK=true
|
ENABLE_NETWORK_PROBE_MOCK=true
|
||||||
ENABLE_TITLE_GENERATION_SKIP=true
|
ENABLE_TITLE_GENERATION_SKIP=true
|
||||||
|
|||||||
@@ -311,7 +311,6 @@ Discord minimum config:
|
|||||||
MESSAGING_PLATFORM="discord"
|
MESSAGING_PLATFORM="discord"
|
||||||
DISCORD_BOT_TOKEN="your-discord-bot-token"
|
DISCORD_BOT_TOKEN="your-discord-bot-token"
|
||||||
ALLOWED_DISCORD_CHANNELS="123456789"
|
ALLOWED_DISCORD_CHANNELS="123456789"
|
||||||
CLAUDE_WORKSPACE=
|
|
||||||
ALLOWED_DIR="C:/Users/yourname/projects"
|
ALLOWED_DIR="C:/Users/yourname/projects"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -323,7 +322,6 @@ Telegram minimum config:
|
|||||||
MESSAGING_PLATFORM="telegram"
|
MESSAGING_PLATFORM="telegram"
|
||||||
TELEGRAM_BOT_TOKEN="123456789:ABC..."
|
TELEGRAM_BOT_TOKEN="123456789:ABC..."
|
||||||
ALLOWED_TELEGRAM_USER_ID="your-user-id"
|
ALLOWED_TELEGRAM_USER_ID="your-user-id"
|
||||||
CLAUDE_WORKSPACE=
|
|
||||||
ALLOWED_DIR="C:/Users/yourname/projects"
|
ALLOWED_DIR="C:/Users/yourname/projects"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -374,7 +372,7 @@ MODEL="nvidia_nim/z-ai/glm4.7"
|
|||||||
ANTHROPIC_AUTH_TOKEN="freecc"
|
ANTHROPIC_AUTH_TOKEN="freecc"
|
||||||
```
|
```
|
||||||
|
|
||||||
Config precedence is repo `.env`, then `~/.fcc/.env`, then `FCC_ENV_FILE` when set. Blank `CLAUDE_WORKSPACE` uses `~/.fcc/agent_workspace`. `ANTHROPIC_AUTH_TOKEN` can be any local secret; pass the same value to Claude Code.
|
Config precedence is repo `.env`, then `~/.fcc/.env`, then `FCC_ENV_FILE` when set. Claude agent data is always stored under `~/.fcc/agent_workspace`. `ANTHROPIC_AUTH_TOKEN` can be any local secret; pass the same value to Claude Code.
|
||||||
|
|
||||||
### 2. Model Routing
|
### 2. Model Routing
|
||||||
|
|
||||||
|
|||||||
@@ -176,14 +176,6 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
|
|||||||
secret=True,
|
secret=True,
|
||||||
description="Z.ai Coding Plan API key.",
|
description="Z.ai Coding Plan API key.",
|
||||||
),
|
),
|
||||||
ConfigFieldSpec(
|
|
||||||
"ZAI_BASE_URL",
|
|
||||||
"Z.ai Base URL",
|
|
||||||
"providers",
|
|
||||||
settings_attr="zai_base_url",
|
|
||||||
default="https://api.z.ai/api/coding/paas/v4",
|
|
||||||
description="Z.ai OpenAI-compatible Coding Plan endpoint.",
|
|
||||||
),
|
|
||||||
ConfigFieldSpec(
|
ConfigFieldSpec(
|
||||||
"LM_STUDIO_BASE_URL",
|
"LM_STUDIO_BASE_URL",
|
||||||
"LM Studio Base URL",
|
"LM Studio Base URL",
|
||||||
@@ -473,15 +465,6 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
|
|||||||
settings_attr="allowed_discord_channels",
|
settings_attr="allowed_discord_channels",
|
||||||
session_sensitive=True,
|
session_sensitive=True,
|
||||||
),
|
),
|
||||||
ConfigFieldSpec(
|
|
||||||
"CLAUDE_WORKSPACE",
|
|
||||||
"Claude Workspace",
|
|
||||||
"messaging",
|
|
||||||
settings_attr="claude_workspace",
|
|
||||||
default="",
|
|
||||||
session_sensitive=True,
|
|
||||||
description="Blank uses ~/.fcc/agent_workspace.",
|
|
||||||
),
|
|
||||||
ConfigFieldSpec(
|
ConfigFieldSpec(
|
||||||
"ALLOWED_DIR",
|
"ALLOWED_DIR",
|
||||||
"Allowed Directory",
|
"Allowed Directory",
|
||||||
@@ -489,14 +472,6 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
|
|||||||
settings_attr="allowed_dir",
|
settings_attr="allowed_dir",
|
||||||
session_sensitive=True,
|
session_sensitive=True,
|
||||||
),
|
),
|
||||||
ConfigFieldSpec(
|
|
||||||
"CLAUDE_CLI_BIN",
|
|
||||||
"Claude CLI Binary",
|
|
||||||
"messaging",
|
|
||||||
settings_attr="claude_cli_bin",
|
|
||||||
default="claude",
|
|
||||||
session_sensitive=True,
|
|
||||||
),
|
|
||||||
ConfigFieldSpec(
|
ConfigFieldSpec(
|
||||||
"MAX_MESSAGE_LOG_ENTRIES_PER_CHAT",
|
"MAX_MESSAGE_LOG_ENTRIES_PER_CHAT",
|
||||||
"Max Message Log Entries",
|
"Max Message Log Entries",
|
||||||
|
|||||||
+20
-23
@@ -100,7 +100,7 @@ textarea {
|
|||||||
|
|
||||||
.section-nav {
|
.section-nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@@ -108,14 +108,15 @@ textarea {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 36px;
|
min-height: 42px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: 8px 10px;
|
padding: 10px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover,
|
.nav-link:hover,
|
||||||
@@ -124,6 +125,11 @@ textarea {
|
|||||||
border-color: var(--line);
|
border-color: var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
border-color: rgba(89, 217, 148, 0.34);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
@@ -188,9 +194,18 @@ textarea {
|
|||||||
border-color: rgba(255, 116, 108, 0.36);
|
border-color: rgba(255, 116, 108, 0.36);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-views,
|
||||||
|
.admin-view {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-view[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.provider-strip,
|
.provider-strip,
|
||||||
.settings-section,
|
.settings-section {
|
||||||
.env-panel {
|
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@@ -378,24 +393,6 @@ textarea {
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-panel {
|
|
||||||
margin-top: 18px;
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.env-preview {
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 360px;
|
|
||||||
margin: 14px 0 0;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #0b0f0d;
|
|
||||||
color: #c9f4dc;
|
|
||||||
padding: 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
+127
-44
@@ -4,9 +4,33 @@ const state = {
|
|||||||
fields: new Map(),
|
fields: new Map(),
|
||||||
localStatus: new Map(),
|
localStatus: new Map(),
|
||||||
modelOptions: [],
|
modelOptions: [],
|
||||||
|
activeView: "providers",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MASKED_SECRET = "********";
|
const MASKED_SECRET = "********";
|
||||||
|
const VIEW_GROUPS = [
|
||||||
|
{
|
||||||
|
id: "providers",
|
||||||
|
label: "Providers",
|
||||||
|
title: "Providers",
|
||||||
|
sections: ["providers", "runtime"],
|
||||||
|
containerId: "providersSections",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "model_config",
|
||||||
|
label: "Model Config",
|
||||||
|
title: "Model Config",
|
||||||
|
sections: ["models", "thinking", "web_tools"],
|
||||||
|
containerId: "modelConfigSections",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "messaging",
|
||||||
|
label: "Messaging",
|
||||||
|
title: "Messaging",
|
||||||
|
sections: ["messaging", "voice"],
|
||||||
|
containerId: "messagingSections",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const byId = (id) => document.getElementById(id);
|
const byId = (id) => document.getElementById(id);
|
||||||
|
|
||||||
@@ -15,11 +39,23 @@ function sourceLabel(source) {
|
|||||||
default: "default",
|
default: "default",
|
||||||
template: "template",
|
template: "template",
|
||||||
repo_env: "repo .env",
|
repo_env: "repo .env",
|
||||||
managed_env: "managed",
|
managed_env: "",
|
||||||
explicit_env_file: "FCC_ENV_FILE",
|
explicit_env_file: "FCC_ENV_FILE",
|
||||||
process: "process env",
|
process: "process env",
|
||||||
};
|
};
|
||||||
return labels[source] || source;
|
return Object.prototype.hasOwnProperty.call(labels, source) ? labels[source] : source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceText(field) {
|
||||||
|
const parts = [];
|
||||||
|
const label = sourceLabel(field.source);
|
||||||
|
if (label) {
|
||||||
|
parts.push(label);
|
||||||
|
}
|
||||||
|
if (field.locked) {
|
||||||
|
parts.push("locked");
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerName(providerId) {
|
function providerName(providerId) {
|
||||||
@@ -70,7 +106,7 @@ async function load() {
|
|||||||
state.status = status;
|
state.status = status;
|
||||||
state.fields = new Map(config.fields.map((field) => [field.key, field]));
|
state.fields = new Map(config.fields.map((field) => [field.key, field]));
|
||||||
updateHeader(status);
|
updateHeader(status);
|
||||||
renderNav(config.sections);
|
renderNav();
|
||||||
renderProviders(config.provider_status);
|
renderProviders(config.provider_status);
|
||||||
renderSections(config.sections, config.fields);
|
renderSections(config.sections, config.fields);
|
||||||
byId("configPath").textContent = config.paths.managed;
|
byId("configPath").textContent = config.paths.managed;
|
||||||
@@ -87,23 +123,51 @@ function updateHeader(status) {
|
|||||||
byId("modelBadge").textContent = status.model || "";
|
byId("modelBadge").textContent = status.model || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNav(sections) {
|
function renderNav() {
|
||||||
const nav = byId("sectionNav");
|
const nav = byId("sectionNav");
|
||||||
nav.innerHTML = "";
|
nav.innerHTML = "";
|
||||||
sections.forEach((section, index) => {
|
VIEW_GROUPS.forEach((view, index) => {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = `nav-link${index === 0 ? " active" : ""}`;
|
button.className = `nav-link${index === 0 ? " active" : ""}`;
|
||||||
button.textContent = section.label;
|
button.dataset.view = view.id;
|
||||||
|
button.textContent = view.label;
|
||||||
|
if (index === 0) {
|
||||||
|
button.setAttribute("aria-current", "page");
|
||||||
|
}
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
document.querySelectorAll(".nav-link").forEach((link) => {
|
setActiveView(view.id, { scroll: true });
|
||||||
link.classList.remove("active");
|
|
||||||
});
|
|
||||||
button.classList.add("active");
|
|
||||||
byId(`section-${section.id}`).scrollIntoView({ behavior: "smooth" });
|
|
||||||
});
|
});
|
||||||
nav.appendChild(button);
|
nav.appendChild(button);
|
||||||
});
|
});
|
||||||
|
setActiveView(state.activeView, { scroll: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveView(viewId, { scroll = false } = {}) {
|
||||||
|
const activeView =
|
||||||
|
VIEW_GROUPS.find((view) => view.id === viewId) || VIEW_GROUPS[0];
|
||||||
|
state.activeView = activeView.id;
|
||||||
|
byId("pageTitle").textContent = activeView.title;
|
||||||
|
|
||||||
|
document.querySelectorAll(".nav-link").forEach((link) => {
|
||||||
|
const selected = link.dataset.view === activeView.id;
|
||||||
|
link.classList.toggle("active", selected);
|
||||||
|
if (selected) {
|
||||||
|
link.setAttribute("aria-current", "page");
|
||||||
|
} else {
|
||||||
|
link.removeAttribute("aria-current");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".admin-view").forEach((view) => {
|
||||||
|
const selected = view.dataset.view === activeView.id;
|
||||||
|
view.classList.toggle("active", selected);
|
||||||
|
view.hidden = !selected;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProviders(providerStatus) {
|
function renderProviders(providerStatus) {
|
||||||
@@ -153,8 +217,11 @@ function updateProviderCard(providerId, status, label, metaText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSections(sections, fields) {
|
function renderSections(sections, fields) {
|
||||||
const container = byId("formSections");
|
VIEW_GROUPS.forEach((view) => {
|
||||||
container.innerHTML = "";
|
byId(view.containerId).innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
||||||
const bySection = new Map();
|
const bySection = new Map();
|
||||||
sections.forEach((section) => bySection.set(section.id, []));
|
sections.forEach((section) => bySection.set(section.id, []));
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
@@ -162,36 +229,43 @@ function renderSections(sections, fields) {
|
|||||||
bySection.get(field.section).push(field);
|
bySection.get(field.section).push(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
sections.forEach((section) => {
|
VIEW_GROUPS.forEach((view) => {
|
||||||
const sectionEl = document.createElement("section");
|
const container = byId(view.containerId);
|
||||||
sectionEl.className = "settings-section";
|
view.sections.forEach((sectionId) => {
|
||||||
sectionEl.id = `section-${section.id}`;
|
const section = sectionById.get(sectionId);
|
||||||
|
const sectionFields = bySection.get(sectionId) || [];
|
||||||
|
if (!section || sectionFields.length === 0) return;
|
||||||
|
|
||||||
const heading = document.createElement("div");
|
const sectionEl = document.createElement("section");
|
||||||
heading.className = "section-heading";
|
sectionEl.className = "settings-section";
|
||||||
heading.innerHTML = `<div><h3>${section.label}</h3><p>${section.description}</p></div>`;
|
sectionEl.id = `section-${section.id}`;
|
||||||
sectionEl.appendChild(heading);
|
|
||||||
|
|
||||||
const grid = document.createElement("div");
|
const heading = document.createElement("div");
|
||||||
grid.className = "field-grid";
|
heading.className = "section-heading";
|
||||||
bySection.get(section.id).forEach((field) => {
|
heading.innerHTML = `<div><h3>${section.label}</h3><p>${section.description}</p></div>`;
|
||||||
grid.appendChild(renderField(field));
|
sectionEl.appendChild(heading);
|
||||||
});
|
|
||||||
sectionEl.appendChild(grid);
|
|
||||||
|
|
||||||
if (bySection.get(section.id).some((field) => field.advanced)) {
|
const grid = document.createElement("div");
|
||||||
const toggle = document.createElement("button");
|
grid.className = "field-grid";
|
||||||
toggle.type = "button";
|
sectionFields.forEach((field) => {
|
||||||
toggle.className = "ghost-button advanced-toggle";
|
grid.appendChild(renderField(field));
|
||||||
toggle.textContent = "Show advanced";
|
|
||||||
toggle.addEventListener("click", () => {
|
|
||||||
const showing = sectionEl.classList.toggle("show-advanced");
|
|
||||||
toggle.textContent = showing ? "Hide advanced" : "Show advanced";
|
|
||||||
});
|
});
|
||||||
sectionEl.appendChild(toggle);
|
sectionEl.appendChild(grid);
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(sectionEl);
|
if (sectionFields.some((field) => field.advanced)) {
|
||||||
|
const toggle = document.createElement("button");
|
||||||
|
toggle.type = "button";
|
||||||
|
toggle.className = "ghost-button advanced-toggle";
|
||||||
|
toggle.textContent = "Show advanced";
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const showing = sectionEl.classList.toggle("show-advanced");
|
||||||
|
toggle.textContent = showing ? "Hide advanced" : "Show advanced";
|
||||||
|
});
|
||||||
|
sectionEl.appendChild(toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(sectionEl);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,9 +276,17 @@ function renderField(field) {
|
|||||||
|
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.htmlFor = `field-${field.key}`;
|
label.htmlFor = `field-${field.key}`;
|
||||||
label.innerHTML = `<span>${field.label}</span><span class="field-source">${sourceLabel(
|
const labelText = document.createElement("span");
|
||||||
field.source,
|
labelText.textContent = field.label;
|
||||||
)}${field.locked ? " locked" : ""}</span>`;
|
label.appendChild(labelText);
|
||||||
|
|
||||||
|
const source = sourceText(field);
|
||||||
|
if (source) {
|
||||||
|
const sourceEl = document.createElement("span");
|
||||||
|
sourceEl.className = "field-source";
|
||||||
|
sourceEl.textContent = source;
|
||||||
|
label.appendChild(sourceEl);
|
||||||
|
}
|
||||||
|
|
||||||
const input = inputForField(field);
|
const input = inputForField(field);
|
||||||
input.id = `field-${field.key}`;
|
input.id = `field-${field.key}`;
|
||||||
@@ -316,7 +398,6 @@ async function validate(showResult = true) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ values: changedValues() }),
|
body: JSON.stringify({ values: changedValues() }),
|
||||||
});
|
});
|
||||||
byId("envPreview").textContent = result.env_preview || "";
|
|
||||||
if (showResult) {
|
if (showResult) {
|
||||||
showValidationResult(result);
|
showValidationResult(result);
|
||||||
}
|
}
|
||||||
@@ -336,7 +417,6 @@ async function apply() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ values: changedValues() }),
|
body: JSON.stringify({ values: changedValues() }),
|
||||||
});
|
});
|
||||||
byId("envPreview").textContent = result.env_preview || "";
|
|
||||||
if (!result.applied) {
|
if (!result.applied) {
|
||||||
showValidationResult(result);
|
showValidationResult(result);
|
||||||
return;
|
return;
|
||||||
@@ -388,7 +468,10 @@ async function testProvider(providerId, button) {
|
|||||||
result.models.slice(0, 3).join(", ") || "No models returned",
|
result.models.slice(0, 3).join(", ") || "No models returned",
|
||||||
);
|
);
|
||||||
state.modelOptions = Array.from(
|
state.modelOptions = Array.from(
|
||||||
new Set([...state.modelOptions, ...result.models.map((model) => `${providerId}/${model}`)]),
|
new Set([
|
||||||
|
...state.modelOptions,
|
||||||
|
...result.models.map((model) => `${providerId}/${model}`),
|
||||||
|
]),
|
||||||
).sort();
|
).sort();
|
||||||
syncModelDatalist();
|
syncModelDatalist();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+34
-19
@@ -17,14 +17,14 @@
|
|||||||
<p>Server Control</p>
|
<p>Server Control</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav id="sectionNav" class="section-nav" aria-label="Settings sections"></nav>
|
<nav id="sectionNav" class="section-nav" aria-label="Admin views"></nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Local Admin</p>
|
<p class="eyebrow">Local Admin</p>
|
||||||
<h2>Runtime Config</h2>
|
<h2 id="pageTitle">Providers</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-state">
|
<div class="server-state">
|
||||||
<span id="serverStatus" class="status-pill neutral">Loading</span>
|
<span id="serverStatus" class="status-pill neutral">Loading</span>
|
||||||
@@ -32,25 +32,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="provider-strip" aria-label="Provider status">
|
<div id="adminViews" class="admin-views">
|
||||||
<div class="strip-header">
|
<section id="view-providers" class="admin-view active" data-view="providers">
|
||||||
<h3>Providers</h3>
|
<section class="provider-strip" aria-label="Provider status">
|
||||||
<button id="refreshLocal" class="ghost-button" type="button">Check local</button>
|
<div class="strip-header">
|
||||||
</div>
|
<h3>Providers</h3>
|
||||||
<div id="providerGrid" class="provider-grid"></div>
|
<button id="refreshLocal" class="ghost-button" type="button">
|
||||||
</section>
|
Check local
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="providerGrid" class="provider-grid"></div>
|
||||||
|
</section>
|
||||||
|
<div
|
||||||
|
id="providersSections"
|
||||||
|
class="form-sections"
|
||||||
|
aria-label="Provider configuration"
|
||||||
|
></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="formSections" class="form-sections" aria-label="Configuration"></section>
|
<section id="view-model_config" class="admin-view" data-view="model_config" hidden>
|
||||||
|
<div
|
||||||
|
id="modelConfigSections"
|
||||||
|
class="form-sections"
|
||||||
|
aria-label="Model configuration"
|
||||||
|
></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="env-panel">
|
<section id="view-messaging" class="admin-view" data-view="messaging" hidden>
|
||||||
<div class="section-heading">
|
<div
|
||||||
<div>
|
id="messagingSections"
|
||||||
<h3>Generated Env</h3>
|
class="form-sections"
|
||||||
<p>Read-only preview of the managed config file.</p>
|
aria-label="Messaging configuration"
|
||||||
</div>
|
></div>
|
||||||
</div>
|
</section>
|
||||||
<pre id="envPreview" class="env-preview"></pre>
|
</div>
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="action-bar">
|
<footer class="action-bar">
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
|
|||||||
credential_env="ZAI_API_KEY",
|
credential_env="ZAI_API_KEY",
|
||||||
credential_attr="zai_api_key",
|
credential_attr="zai_api_key",
|
||||||
default_base_url=ZAI_DEFAULT_BASE,
|
default_base_url=ZAI_DEFAULT_BASE,
|
||||||
base_url_attr="zai_base_url",
|
|
||||||
proxy_attr="zai_proxy",
|
proxy_attr="zai_proxy",
|
||||||
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
|
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
|
||||||
),
|
),
|
||||||
|
|||||||
+11
-15
@@ -124,10 +124,6 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# ==================== Z.ai Config ====================
|
# ==================== Z.ai Config ====================
|
||||||
zai_api_key: str = Field(default="", validation_alias="ZAI_API_KEY")
|
zai_api_key: str = Field(default="", validation_alias="ZAI_API_KEY")
|
||||||
zai_base_url: str = Field(
|
|
||||||
default="https://api.z.ai/api/coding/paas/v4",
|
|
||||||
validation_alias="ZAI_BASE_URL",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ==================== Messaging Platform Selection ====================
|
# ==================== Messaging Platform Selection ====================
|
||||||
# Valid: "telegram" | "discord" | "none"
|
# Valid: "telegram" | "discord" | "none"
|
||||||
@@ -297,12 +293,7 @@ class Settings(BaseSettings):
|
|||||||
allowed_discord_channels: str | None = Field(
|
allowed_discord_channels: str | None = Field(
|
||||||
default=None, validation_alias="ALLOWED_DISCORD_CHANNELS"
|
default=None, validation_alias="ALLOWED_DISCORD_CHANNELS"
|
||||||
)
|
)
|
||||||
claude_workspace: str = Field(
|
|
||||||
default_factory=lambda: str(default_claude_workspace_path()),
|
|
||||||
validation_alias="CLAUDE_WORKSPACE",
|
|
||||||
)
|
|
||||||
allowed_dir: str = ""
|
allowed_dir: str = ""
|
||||||
claude_cli_bin: str = Field(default="claude", validation_alias="CLAUDE_CLI_BIN")
|
|
||||||
max_message_log_entries_per_chat: int | None = Field(
|
max_message_log_entries_per_chat: int | None = Field(
|
||||||
default=None, validation_alias="MAX_MESSAGE_LOG_ENTRIES_PER_CHAT"
|
default=None, validation_alias="MAX_MESSAGE_LOG_ENTRIES_PER_CHAT"
|
||||||
)
|
)
|
||||||
@@ -351,12 +342,17 @@ class Settings(BaseSettings):
|
|||||||
return None
|
return None
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator("claude_workspace", mode="before")
|
@property
|
||||||
@classmethod
|
def claude_workspace(self) -> str:
|
||||||
def default_blank_claude_workspace(cls, v: Any) -> Any:
|
"""Return the fixed Claude data workspace path."""
|
||||||
if v == "" or v is None:
|
|
||||||
return str(default_claude_workspace_path())
|
return str(default_claude_workspace_path())
|
||||||
return v
|
|
||||||
|
@property
|
||||||
|
def claude_cli_bin(self) -> str:
|
||||||
|
"""Return the fixed Claude Code binary name."""
|
||||||
|
|
||||||
|
return "claude"
|
||||||
|
|
||||||
@field_validator("whisper_device")
|
@field_validator("whisper_device")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ def _clear_process_config(monkeypatch) -> None:
|
|||||||
"HOST",
|
"HOST",
|
||||||
"PORT",
|
"PORT",
|
||||||
"LOG_FILE",
|
"LOG_FILE",
|
||||||
|
"ZAI_BASE_URL",
|
||||||
|
"CLAUDE_WORKSPACE",
|
||||||
|
"CLAUDE_CLI_BIN",
|
||||||
):
|
):
|
||||||
monkeypatch.delenv(key, raising=False)
|
monkeypatch.delenv(key, raising=False)
|
||||||
|
|
||||||
@@ -45,6 +48,26 @@ def test_admin_page_is_loopback_only(monkeypatch, tmp_path):
|
|||||||
assert remote_client.get("/admin").status_code == 403
|
assert remote_client.get("/admin").status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_page_no_longer_renders_generated_env_panel(monkeypatch, tmp_path):
|
||||||
|
_set_home(monkeypatch, tmp_path)
|
||||||
|
app = create_app(lifespan_enabled=False)
|
||||||
|
|
||||||
|
response = _local_client(app).get("/admin")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Generated Env" not in response.text
|
||||||
|
assert "envPreview" not in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_static_hides_managed_source_label():
|
||||||
|
script = Path("api/admin_static/admin.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert 'managed_env: "",' in script
|
||||||
|
assert "hasOwnProperty.call(labels, source)" in script
|
||||||
|
assert 'parts.push("locked")' in script
|
||||||
|
assert "sourceEl.textContent = source" in script
|
||||||
|
|
||||||
|
|
||||||
def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path):
|
def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path):
|
||||||
_set_home(monkeypatch, tmp_path)
|
_set_home(monkeypatch, tmp_path)
|
||||||
_clear_process_config(monkeypatch)
|
_clear_process_config(monkeypatch)
|
||||||
@@ -57,6 +80,9 @@ def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path):
|
|||||||
keys = {field["key"] for field in body["fields"]}
|
keys = {field["key"] for field in body["fields"]}
|
||||||
assert "ANTHROPIC_AUTH_TOKEN" in keys
|
assert "ANTHROPIC_AUTH_TOKEN" in keys
|
||||||
assert "OPENROUTER_API_KEY" in keys
|
assert "OPENROUTER_API_KEY" in keys
|
||||||
|
assert "ZAI_BASE_URL" not in keys
|
||||||
|
assert "CLAUDE_WORKSPACE" not in keys
|
||||||
|
assert "CLAUDE_CLI_BIN" not in keys
|
||||||
assert "LOG_FILE" not in keys
|
assert "LOG_FILE" not in keys
|
||||||
auth_field = next(
|
auth_field = next(
|
||||||
field for field in body["fields"] if field["key"] == "ANTHROPIC_AUTH_TOKEN"
|
field for field in body["fields"] if field["key"] == "ANTHROPIC_AUTH_TOKEN"
|
||||||
@@ -66,6 +92,23 @@ def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path):
|
|||||||
assert auth_field["source"] == "template"
|
assert auth_field["source"] == "template"
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_config_preserves_managed_env_source_contract(monkeypatch, tmp_path):
|
||||||
|
_set_home(monkeypatch, tmp_path)
|
||||||
|
_clear_process_config(monkeypatch)
|
||||||
|
env_file = tmp_path / ".fcc" / ".env"
|
||||||
|
env_file.parent.mkdir(parents=True)
|
||||||
|
env_file.write_text("MODEL=open_router/managed-model\n", encoding="utf-8")
|
||||||
|
app = create_app(lifespan_enabled=False)
|
||||||
|
|
||||||
|
response = _local_client(app).get("/admin/api/config")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
model_field = next(field for field in body["fields"] if field["key"] == "MODEL")
|
||||||
|
assert model_field["source"] == "managed_env"
|
||||||
|
assert model_field["locked"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_admin_validate_rejects_bad_model_shape(monkeypatch, tmp_path):
|
def test_admin_validate_rejects_bad_model_shape(monkeypatch, tmp_path):
|
||||||
_set_home(monkeypatch, tmp_path)
|
_set_home(monkeypatch, tmp_path)
|
||||||
_clear_process_config(monkeypatch)
|
_clear_process_config(monkeypatch)
|
||||||
@@ -116,6 +159,103 @@ def test_admin_apply_writes_complete_managed_env_and_masks_preview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_apply_preserves_hidden_diagnostics_and_smoke_values(
|
||||||
|
monkeypatch, tmp_path
|
||||||
|
):
|
||||||
|
_set_home(monkeypatch, tmp_path)
|
||||||
|
_clear_process_config(monkeypatch)
|
||||||
|
env_file = tmp_path / ".fcc" / ".env"
|
||||||
|
env_file.parent.mkdir(parents=True)
|
||||||
|
env_file.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"MODEL=nvidia_nim/old-model",
|
||||||
|
"LOG_RAW_API_PAYLOADS=true",
|
||||||
|
"FCC_SMOKE_MODEL_ZAI=zai/smoke-model",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
app = create_app(lifespan_enabled=False)
|
||||||
|
|
||||||
|
response = _local_client(app).post(
|
||||||
|
"/admin/api/config/apply",
|
||||||
|
json={"values": {"MODEL": "open_router/test-model"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
assert body["applied"] is True
|
||||||
|
text = env_file.read_text("utf-8")
|
||||||
|
assert "MODEL=open_router/test-model" in text
|
||||||
|
assert "LOG_RAW_API_PAYLOADS=true" in text
|
||||||
|
assert "FCC_SMOKE_MODEL_ZAI=zai/smoke-model" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_apply_omits_stale_zai_base_url(monkeypatch, tmp_path):
|
||||||
|
_set_home(monkeypatch, tmp_path)
|
||||||
|
_clear_process_config(monkeypatch)
|
||||||
|
env_file = tmp_path / ".fcc" / ".env"
|
||||||
|
env_file.parent.mkdir(parents=True)
|
||||||
|
env_file.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"MODEL=zai/glm-5.1",
|
||||||
|
"ZAI_API_KEY=zai-secret",
|
||||||
|
"ZAI_BASE_URL=https://custom.zai.invalid/v1",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
app = create_app(lifespan_enabled=False)
|
||||||
|
|
||||||
|
response = _local_client(app).post(
|
||||||
|
"/admin/api/config/apply",
|
||||||
|
json={"values": {"MODEL": "zai/glm-5.1"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
assert body["applied"] is True
|
||||||
|
text = env_file.read_text("utf-8")
|
||||||
|
assert "ZAI_API_KEY=zai-secret" in text
|
||||||
|
assert "ZAI_BASE_URL" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_apply_omits_stale_fixed_claude_runtime_settings(monkeypatch, tmp_path):
|
||||||
|
_set_home(monkeypatch, tmp_path)
|
||||||
|
_clear_process_config(monkeypatch)
|
||||||
|
env_file = tmp_path / ".fcc" / ".env"
|
||||||
|
env_file.parent.mkdir(parents=True)
|
||||||
|
env_file.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"MODEL=open_router/test-model",
|
||||||
|
"CLAUDE_WORKSPACE=C:/custom/workspace",
|
||||||
|
"CLAUDE_CLI_BIN=claude-custom",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
app = create_app(lifespan_enabled=False)
|
||||||
|
|
||||||
|
response = _local_client(app).post(
|
||||||
|
"/admin/api/config/apply",
|
||||||
|
json={"values": {"MODEL": "open_router/test-model"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
assert body["applied"] is True
|
||||||
|
text = env_file.read_text("utf-8")
|
||||||
|
assert "MODEL=open_router/test-model" in text
|
||||||
|
assert "CLAUDE_WORKSPACE" not in text
|
||||||
|
assert "CLAUDE_CLI_BIN" not in text
|
||||||
|
|
||||||
|
|
||||||
def test_admin_apply_restart_required_reports_automatic_restart(monkeypatch, tmp_path):
|
def test_admin_apply_restart_required_reports_automatic_restart(monkeypatch, tmp_path):
|
||||||
_set_home(monkeypatch, tmp_path)
|
_set_home(monkeypatch, tmp_path)
|
||||||
_clear_process_config(monkeypatch)
|
_clear_process_config(monkeypatch)
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ def _launcher_settings(
|
|||||||
*,
|
*,
|
||||||
port: int = 8082,
|
port: int = 8082,
|
||||||
token: str = "freecc",
|
token: str = "freecc",
|
||||||
claude_bin: str = "claude-test",
|
|
||||||
) -> Settings:
|
) -> Settings:
|
||||||
return Settings.model_construct(
|
return Settings.model_construct(
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=port,
|
port=port,
|
||||||
anthropic_auth_token=token,
|
anthropic_auth_token=token,
|
||||||
claude_cli_bin=claude_bin,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -343,7 +341,7 @@ def test_launch_claude_exits_when_command_cannot_be_resolved(
|
|||||||
) -> None:
|
) -> None:
|
||||||
from cli.entrypoints import launch_claude
|
from cli.entrypoints import launch_claude
|
||||||
|
|
||||||
settings = _launcher_settings(claude_bin="claude-missing")
|
settings = _launcher_settings()
|
||||||
with (
|
with (
|
||||||
patch("cli.entrypoints.get_settings", return_value=settings),
|
patch("cli.entrypoints.get_settings", return_value=settings),
|
||||||
patch("cli.entrypoints._preflight_proxy", return_value=None),
|
patch("cli.entrypoints._preflight_proxy", return_value=None),
|
||||||
@@ -356,7 +354,7 @@ def test_launch_claude_exits_when_command_cannot_be_resolved(
|
|||||||
assert exc_info.value.code == 127
|
assert exc_info.value.code == 127
|
||||||
popen.assert_not_called()
|
popen.assert_not_called()
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "Could not find Claude Code command: claude-missing" in captured.err
|
assert "Could not find Claude Code command: claude" in captured.err
|
||||||
assert "npm install -g @anthropic-ai/claude-code" in captured.err
|
assert "npm install -g @anthropic-ai/claude-code" in captured.err
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Tests for config/settings.py and config/nim.py"""
|
"""Tests for config/settings.py and config/nim.py"""
|
||||||
|
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ class TestSettings:
|
|||||||
assert settings.debug_subagent_stack is False
|
assert settings.debug_subagent_stack is False
|
||||||
|
|
||||||
def test_default_claude_workspace_uses_fcc_home(self, monkeypatch, tmp_path):
|
def test_default_claude_workspace_uses_fcc_home(self, monkeypatch, tmp_path):
|
||||||
"""Blank/unset CLAUDE_WORKSPACE stores agent data under ~/.fcc."""
|
"""Unset CLAUDE_WORKSPACE stores agent data under ~/.fcc."""
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
|
|
||||||
monkeypatch.setenv("HOME", str(tmp_path))
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
@@ -77,8 +79,19 @@ class TestSettings:
|
|||||||
|
|
||||||
assert not hasattr(settings, "log_file")
|
assert not hasattr(settings, "log_file")
|
||||||
|
|
||||||
|
def test_stale_zai_base_url_env_is_ignored(self, monkeypatch):
|
||||||
|
"""Cloud Z.ai endpoint is fixed in provider metadata, not settings."""
|
||||||
|
from config.settings import Settings
|
||||||
|
|
||||||
|
monkeypatch.setenv("ZAI_BASE_URL", "https://custom.zai.invalid/v1")
|
||||||
|
monkeypatch.setitem(Settings.model_config, "env_file", ())
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
assert not hasattr(settings, "zai_base_url")
|
||||||
|
|
||||||
def test_blank_claude_workspace_uses_fcc_home(self, monkeypatch, tmp_path):
|
def test_blank_claude_workspace_uses_fcc_home(self, monkeypatch, tmp_path):
|
||||||
"""An explicit blank env value keeps the default workspace path."""
|
"""An explicit blank env value does not affect the fixed workspace path."""
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
|
|
||||||
monkeypatch.setenv("HOME", str(tmp_path))
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
@@ -90,17 +103,51 @@ class TestSettings:
|
|||||||
|
|
||||||
assert settings.claude_workspace == str(tmp_path / ".fcc" / "agent_workspace")
|
assert settings.claude_workspace == str(tmp_path / ".fcc" / "agent_workspace")
|
||||||
|
|
||||||
def test_explicit_claude_workspace_is_preserved(self, monkeypatch, tmp_path):
|
def test_explicit_claude_workspace_is_ignored(self, monkeypatch, tmp_path):
|
||||||
"""Custom CLAUDE_WORKSPACE values are not rewritten."""
|
"""Custom CLAUDE_WORKSPACE values do not override the fixed workspace."""
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
|
|
||||||
workspace = tmp_path / "custom-workspace"
|
workspace = tmp_path / "custom-workspace"
|
||||||
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
||||||
monkeypatch.setenv("CLAUDE_WORKSPACE", str(workspace))
|
monkeypatch.setenv("CLAUDE_WORKSPACE", str(workspace))
|
||||||
monkeypatch.setitem(Settings.model_config, "env_file", ())
|
monkeypatch.setitem(Settings.model_config, "env_file", ())
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
assert settings.claude_workspace == str(workspace)
|
assert settings.claude_workspace == str(tmp_path / ".fcc" / "agent_workspace")
|
||||||
|
|
||||||
|
def test_explicit_claude_cli_bin_is_ignored(self, monkeypatch):
|
||||||
|
"""Custom CLAUDE_CLI_BIN values do not override the fixed binary."""
|
||||||
|
from config.settings import Settings
|
||||||
|
|
||||||
|
monkeypatch.setenv("CLAUDE_CLI_BIN", "claude-custom")
|
||||||
|
monkeypatch.setitem(Settings.model_config, "env_file", ())
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
assert settings.claude_cli_bin == "claude"
|
||||||
|
|
||||||
|
def test_direct_claude_runtime_overrides_are_ignored(self, monkeypatch, tmp_path):
|
||||||
|
"""Constructor extras cannot override fixed Claude runtime settings."""
|
||||||
|
from config.settings import Settings
|
||||||
|
|
||||||
|
monkeypatch.setenv("HOME", str(tmp_path))
|
||||||
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
||||||
|
monkeypatch.setitem(Settings.model_config, "env_file", ())
|
||||||
|
|
||||||
|
settings = Settings(
|
||||||
|
**cast(
|
||||||
|
Any,
|
||||||
|
{
|
||||||
|
"claude_workspace": str(tmp_path / "custom-workspace"),
|
||||||
|
"claude_cli_bin": "claude-custom",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert settings.claude_workspace == str(tmp_path / ".fcc" / "agent_workspace")
|
||||||
|
assert settings.claude_cli_bin == "claude"
|
||||||
|
|
||||||
def test_get_settings_cached(self):
|
def test_get_settings_cached(self):
|
||||||
"""Test get_settings returns cached instance."""
|
"""Test get_settings returns cached instance."""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from config.nim import NimSettings
|
from config.nim import NimSettings
|
||||||
|
from config.provider_catalog import ZAI_DEFAULT_BASE
|
||||||
from config.provider_ids import SUPPORTED_PROVIDER_IDS
|
from config.provider_ids import SUPPORTED_PROVIDER_IDS
|
||||||
from providers.deepseek import DeepSeekProvider
|
from providers.deepseek import DeepSeekProvider
|
||||||
from providers.exceptions import UnknownProviderTypeError
|
from providers.exceptions import UnknownProviderTypeError
|
||||||
@@ -17,6 +18,7 @@ from providers.opencode import OpenCodeProvider
|
|||||||
from providers.registry import (
|
from providers.registry import (
|
||||||
PROVIDER_DESCRIPTORS,
|
PROVIDER_DESCRIPTORS,
|
||||||
ProviderRegistry,
|
ProviderRegistry,
|
||||||
|
build_provider_config,
|
||||||
create_provider,
|
create_provider,
|
||||||
)
|
)
|
||||||
from providers.wafer import WaferProvider
|
from providers.wafer import WaferProvider
|
||||||
@@ -89,6 +91,24 @@ def test_ollama_descriptor_uses_native_anthropic_transport():
|
|||||||
assert "native_anthropic" in descriptor.capabilities
|
assert "native_anthropic" in descriptor.capabilities
|
||||||
|
|
||||||
|
|
||||||
|
def test_zai_descriptor_uses_fixed_cloud_base_url():
|
||||||
|
descriptor = PROVIDER_DESCRIPTORS["zai"]
|
||||||
|
|
||||||
|
assert descriptor.default_base_url == ZAI_DEFAULT_BASE
|
||||||
|
assert descriptor.base_url_attr is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_zai_provider_config_ignores_stale_base_url_setting():
|
||||||
|
descriptor = PROVIDER_DESCRIPTORS["zai"]
|
||||||
|
|
||||||
|
config = build_provider_config(
|
||||||
|
descriptor,
|
||||||
|
_make_settings(zai_base_url="https://custom.zai.invalid/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config.base_url == ZAI_DEFAULT_BASE
|
||||||
|
|
||||||
|
|
||||||
def test_create_provider_uses_native_openrouter_by_default():
|
def test_create_provider_uses_native_openrouter_by_default():
|
||||||
with patch("httpx.AsyncClient"):
|
with patch("httpx.AsyncClient"):
|
||||||
provider = create_provider("open_router", _make_settings())
|
provider = create_provider("open_router", _make_settings())
|
||||||
|
|||||||
Reference in New Issue
Block a user