mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-10 00:14:16 +00:00
## Problem
FCC does not expose Cohere's OpenAI-compatible chat models, so users
with Cohere keys cannot route Claude, Codex, or messaging prompts
through Cohere.
## Changes
| Before | After |
| --- | --- |
| Provider catalog did not include Cohere. | Provider catalog includes
Cohere with `COHERE_API_KEY`, `COHERE_PROXY`, admin status, and smoke
model wiring. |
| Requests could not target Cohere's compatibility API. |
`providers/cohere` routes OpenAI-chat requests to Cohere's compatibility
API with Cohere-specific request policy. |
| Docs and templates omitted Cohere setup. | README, `.env.example`, and
architecture docs document Cohere setup and ownership. |
| Cohere behavior had no regression coverage. | Provider, runtime,
admin, config, smoke, and catalog tests cover Cohere integration. |
<!-- greptile_comment -->
<details open><summary><h3>Greptile Summary</h3></summary>
This PR adds Cohere as a new OpenAI-compatible chat provider. The main
changes are:
- Cohere provider metadata in the catalog, settings, Admin UI manifest,
and runtime factory.
- A new `CohereProvider` using the shared OpenAI chat transport with
Cohere-specific request shaping.
- Cohere API key, proxy, smoke model, README, architecture, and
environment template updates.
- Tests for Admin config, settings, provider catalog order, smoke
config, runtime creation, and Cohere request/stream behavior.
- Version and lockfile updates for the new provider feature.
</details>
<h3>Confidence Score: 5/5</h3>
Safe to merge with minimal risk.
No functional, security, or contract issues were identified. Cohere is
consistently wired through settings, catalog metadata, factory creation,
Admin config, smoke defaults, docs, versioning, and targeted tests. The
implemented Cohere `reasoning_effort` values match the Compatibility API
behavior checked during review.
No files require special attention.
<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>
**What T-Rex did**
- Validated the provider runtime handling of Cohere requests, including
the request body policy, streaming parsing, and default base URL and API
key behavior.
- Verified that the runtime descriptor wiring and provider config
proxy/key behavior pass in the general contract validation.
- Confirmed the admin/config smoke contract artifact shows the Cohere
environment settings, admin config masking, feature/provider catalog
contracts, and smoke configuration passing.
- Compared the initial -01-before.log and the clean -02-after.log
captures to confirm the same scoped commands are present and both exit
with code 0.
<a
href="https://app.greptile.com/trex/runs/13309850/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a>
<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>
<details open><summary><h3>Important Files Changed</h3></summary>
| Filename | Overview |
|----------|----------|
| providers/cohere/client.py | Implements Cohere request shaping over
shared OpenAI chat transport, including allowed extra body and reasoning
mapping; no issues found. |
| config/provider_catalog.py | Registers Cohere with credential, proxy,
base URL, transport, and capability metadata; no issues found. |
| providers/runtime/factory.py | Wires Cohere into runtime provider
factory dispatch; no issues found. |
| config/settings.py | Adds Cohere API key and proxy settings aliases;
no issues found. |
| api/admin_config/provider_manifest.py | Adds Cohere API key
labeling/description through catalog-derived Admin fields; no issues
found. |
| smoke/lib/config.py | Adds Cohere default smoke model and credential
detection; no issues found. |
| tests/providers/test_cohere.py | Adds request-policy and streaming
adapter tests for the Cohere provider; no issues found. |
| tests/providers/test_provider_runtime.py | Adds Cohere descriptor,
config build, and factory instantiation coverage; no issues found. |
| README.md | Adds Cohere setup instructions and updates provider
counts/order; no issues found. |
| pyproject.toml | Bumps package version for the new provider feature;
no issues found. |
| uv.lock | Updates the lockfile package version to match
`pyproject.toml`; no issues found. |
</details>
<details open><summary><h3>Sequence Diagram</h3></summary>
<a href="#gh-light-mode-only">
```mermaid
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User as User/Admin config
participant Catalog as Provider catalog/settings
participant Factory as Runtime factory
participant Cohere as CohereProvider
participant Transport as OpenAI chat transport
participant API as Cohere Compatibility API
User->>Catalog: "Configure MODEL=cohere/... and COHERE_API_KEY"
Catalog->>Factory: Build ProviderConfig for provider_id cohere
Factory->>Cohere: Instantiate CohereProvider(config)
Cohere->>Transport: Build chat body with Cohere policy
Transport->>API: "POST /chat/completions stream=true"
API-->>Transport: Streaming OpenAI-compatible chunks
Transport-->>User: Anthropic SSE events
```
</a>
<a href="#gh-dark-mode-only">
```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User as User/Admin config
participant Catalog as Provider catalog/settings
participant Factory as Runtime factory
participant Cohere as CohereProvider
participant Transport as OpenAI chat transport
participant API as Cohere Compatibility API
User->>Catalog: "Configure MODEL=cohere/... and COHERE_API_KEY"
Catalog->>Factory: Build ProviderConfig for provider_id cohere
Factory->>Cohere: Instantiate CohereProvider(config)
Cohere->>Transport: Build chat body with Cohere policy
Transport->>API: "POST /chat/completions stream=true"
API-->>Transport: Streaming OpenAI-compatible chunks
Transport-->>User: Anthropic SSE events
```
</a>
</details>
<sub>Reviews (1): Last reviewed commit: ["Add Cohere
provider"](7956802968)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=41887611)</sub>
<!-- /greptile_comment -->
592 lines
19 KiB
Python
592 lines
19 KiB
Python
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import httpx
|
|
from fastapi.testclient import TestClient
|
|
|
|
from api.admin_config.values import MASKED_SECRET
|
|
from api.admin_urls import local_admin_url
|
|
from api.app import create_app
|
|
from config.settings import Settings
|
|
|
|
|
|
def _local_client(app):
|
|
return TestClient(app, client=("127.0.0.1", 50000))
|
|
|
|
|
|
def _set_home(monkeypatch, tmp_path: Path) -> None:
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
|
|
def _clear_process_config(monkeypatch) -> None:
|
|
for key in (
|
|
"MODEL",
|
|
"NVIDIA_NIM_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
"ANTHROPIC_AUTH_TOKEN",
|
|
"FCC_ENV_FILE",
|
|
"CLOUDFLARE_API_TOKEN",
|
|
"CLOUDFLARE_ACCOUNT_ID",
|
|
"HOST",
|
|
"PORT",
|
|
"LOG_FILE",
|
|
"ZAI_BASE_URL",
|
|
"CLAUDE_WORKSPACE",
|
|
"CLAUDE_CLI_BIN",
|
|
):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
def test_admin_page_is_loopback_only(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
assert _local_client(app).get("/admin").status_code == 200
|
|
remote_client = TestClient(app, client=("203.0.113.10", 50000))
|
|
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_page_no_longer_renders_global_status_header(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 "Local Admin" not in response.text
|
|
assert "serverStatus" not in response.text
|
|
assert "modelBadge" not in response.text
|
|
|
|
|
|
def test_admin_static_no_longer_fetches_global_status_header():
|
|
script = Path("api/admin_static/admin.js").read_text(encoding="utf-8")
|
|
|
|
assert 'api("/admin/api/status")' not in script
|
|
assert "updateHeader" not in script
|
|
assert '"Running"' not in script
|
|
assert "serverStatus" not in script
|
|
assert "modelBadge" not in script
|
|
|
|
|
|
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):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).get("/admin/api/config")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
keys = {field["key"] for field in body["fields"]}
|
|
assert "ANTHROPIC_AUTH_TOKEN" in keys
|
|
assert "OPENROUTER_API_KEY" in keys
|
|
assert "FIREWORKS_API_KEY" in keys
|
|
assert "CLOUDFLARE_API_TOKEN" in keys
|
|
assert "CLOUDFLARE_ACCOUNT_ID" in keys
|
|
assert "GEMINI_API_KEY" in keys
|
|
assert "GROQ_API_KEY" in keys
|
|
assert "CEREBRAS_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
|
|
auth_field = next(
|
|
field for field in body["fields"] if field["key"] == "ANTHROPIC_AUTH_TOKEN"
|
|
)
|
|
assert auth_field["secret"] is True
|
|
assert auth_field["value"] == MASKED_SECRET
|
|
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):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/validate",
|
|
json={"values": {"MODEL": "missing-provider-prefix"}},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["valid"] is False
|
|
assert any("provider type" in error for error in body["errors"])
|
|
|
|
|
|
def test_admin_apply_writes_complete_managed_env_and_masks_preview(
|
|
monkeypatch, tmp_path
|
|
):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "open_router/test-model",
|
|
"OPENROUTER_API_KEY": "router-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "OPENROUTER_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text("utf-8")
|
|
assert "MODEL=open_router/test-model" in text
|
|
assert "OPENROUTER_API_KEY=router-secret" in text
|
|
assert "ANTHROPIC_AUTH_TOKEN=" in text
|
|
assert body["restart"] == {
|
|
"required": False,
|
|
"automatic": False,
|
|
"admin_url": None,
|
|
"fields": [],
|
|
}
|
|
|
|
|
|
def test_admin_apply_writes_fireworks_key_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "fireworks/test-model",
|
|
"FIREWORKS_API_KEY": "fw-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "FIREWORKS_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=fireworks/test-model" in text
|
|
assert "FIREWORKS_API_KEY=fw-secret" in text
|
|
|
|
|
|
def test_admin_apply_writes_gemini_key_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "gemini/models/gemini-3.1-flash-lite",
|
|
"GEMINI_API_KEY": "gm-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "GEMINI_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=gemini/models/gemini-3.1-flash-lite" in text
|
|
assert "GEMINI_API_KEY=gm-secret" in text
|
|
|
|
|
|
def test_admin_apply_writes_groq_key_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "groq/llama-3.3-70b-versatile",
|
|
"GROQ_API_KEY": "gq-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "GROQ_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=groq/llama-3.3-70b-versatile" in text
|
|
assert "GROQ_API_KEY=gq-secret" in text
|
|
|
|
|
|
def test_admin_apply_writes_cerebras_key_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "cerebras/llama3.1-8b",
|
|
"CEREBRAS_API_KEY": "cb-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "CEREBRAS_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=cerebras/llama3.1-8b" in text
|
|
assert "CEREBRAS_API_KEY=cb-secret" in text
|
|
|
|
|
|
def test_admin_apply_writes_cloudflare_fields_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "cloudflare/@cf/moonshotai/kimi-k2.6",
|
|
"CLOUDFLARE_API_TOKEN": "cf-secret",
|
|
"CLOUDFLARE_ACCOUNT_ID": "cf-account",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "CLOUDFLARE_API_TOKEN=********" in body["env_preview"]
|
|
assert "CLOUDFLARE_ACCOUNT_ID=cf-account" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=cloudflare/@cf/moonshotai/kimi-k2.6" in text
|
|
assert "CLOUDFLARE_API_TOKEN=cf-secret" in text
|
|
assert "CLOUDFLARE_ACCOUNT_ID=cf-account" in text
|
|
|
|
|
|
def test_admin_apply_writes_huggingface_key_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "huggingface/openai/gpt-oss-120b:fastest",
|
|
"HUGGINGFACE_API_KEY": "hf-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "HUGGINGFACE_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=huggingface/openai/gpt-oss-120b:fastest" in text
|
|
assert "HUGGINGFACE_API_KEY=hf-secret" in text
|
|
|
|
|
|
def test_admin_apply_writes_cohere_key_and_masks_preview(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={
|
|
"values": {
|
|
"MODEL": "cohere/command-a-plus-05-2026",
|
|
"COHERE_API_KEY": "cohere-secret",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert "COHERE_API_KEY=********" in body["env_preview"]
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
text = env_file.read_text(encoding="utf-8")
|
|
assert "MODEL=cohere/command-a-plus-05-2026" in text
|
|
assert "COHERE_API_KEY=cohere-secret" in text
|
|
|
|
|
|
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):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
callbacks: list[str] = []
|
|
|
|
async def restart_callback() -> None:
|
|
callbacks.append("restart")
|
|
|
|
app.state.admin_restart_callback = restart_callback
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={"values": {"PORT": "9090"}},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert body["pending_fields"] == ["PORT"]
|
|
assert body["restart"] == {
|
|
"required": True,
|
|
"automatic": True,
|
|
"admin_url": "http://127.0.0.1:9090/admin",
|
|
"fields": ["PORT"],
|
|
}
|
|
assert callbacks == ["restart"]
|
|
|
|
|
|
def test_admin_apply_restart_required_reports_manual_fallback(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={"values": {"PORT": "9091"}},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["applied"] is True
|
|
assert body["pending_fields"] == ["PORT"]
|
|
assert body["restart"] == {
|
|
"required": True,
|
|
"automatic": False,
|
|
"admin_url": None,
|
|
"fields": ["PORT"],
|
|
}
|
|
|
|
|
|
def test_admin_process_env_values_are_locked_and_not_written(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
monkeypatch.setenv("MODEL", "open_router/process-model")
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
config = _local_client(app).get("/admin/api/config").json()
|
|
model_field = next(field for field in config["fields"] if field["key"] == "MODEL")
|
|
assert model_field["locked"] is True
|
|
assert model_field["source"] == "process"
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={"values": {"MODEL": "deepseek/managed-model"}},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
env_file = tmp_path / ".fcc" / ".env"
|
|
assert "deepseek/managed-model" not in env_file.read_text("utf-8")
|
|
|
|
|
|
def test_admin_first_apply_migrates_repo_env(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
monkeypatch.chdir(tmp_path)
|
|
(tmp_path / ".env").write_text(
|
|
"MODEL=deepseek/deepseek-chat\nDEEPSEEK_API_KEY=deepseek-secret\n",
|
|
encoding="utf-8",
|
|
)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
config = _local_client(app).get("/admin/api/config").json()
|
|
model_field = next(field for field in config["fields"] if field["key"] == "MODEL")
|
|
assert model_field["value"] == "deepseek/deepseek-chat"
|
|
assert model_field["source"] == "repo_env"
|
|
|
|
response = _local_client(app).post(
|
|
"/admin/api/config/apply",
|
|
json={"values": {}},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
managed_text = (tmp_path / ".fcc" / ".env").read_text("utf-8")
|
|
assert "MODEL=deepseek/deepseek-chat" in managed_text
|
|
assert "DEEPSEEK_API_KEY=deepseek-secret" in managed_text
|
|
|
|
|
|
def test_admin_local_provider_status_reports_reachable(monkeypatch, tmp_path):
|
|
_set_home(monkeypatch, tmp_path)
|
|
_clear_process_config(monkeypatch)
|
|
app = create_app(lifespan_enabled=False)
|
|
|
|
class FakeAsyncClient:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *args):
|
|
return None
|
|
|
|
async def get(self, url: str):
|
|
return httpx.Response(200, json={"data": []})
|
|
|
|
with patch("api.admin_routes.httpx.AsyncClient", FakeAsyncClient):
|
|
response = _local_client(app).get("/admin/api/providers/local-status")
|
|
|
|
assert response.status_code == 200
|
|
providers = response.json()["providers"]
|
|
assert {provider["status"] for provider in providers} == {"reachable"}
|
|
|
|
|
|
def test_admin_launch_url_uses_loopback_for_wildcard_host():
|
|
settings = Settings.model_construct(host="0.0.0.0", port=8082)
|
|
|
|
assert local_admin_url(settings) == "http://127.0.0.1:8082/admin"
|