Status LEDs, image model selection, backup restore, provider test, monthly versions
- Sidebar: three status LEDs (server, models, MCP) with an overview popover (/api/status) - Images: preview stage, purpose-based model choice and reference images (Nano Banana Pro edit refines the chosen preview) - Settings: restore a stored or uploaded backup (staged, applied on restart) - Provider form: test connection; saving or reloading re-checks the status at once - opencode Zen/Go: send x-opencode-session with a stable id per conversation - Releases are named vYYYYMM-NNNNN; scripts/release.sh numbers, tags, builds and publishes - Fix: backups made in the same second no longer overwrite each other Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49d404eec7
commit
1f69d63b09
35 files changed
+1156
-88
No files matched your search
@@ -168,7 +168,8 @@ impl Provider for Anthropic {
|
||||
for (k, v) in &req.model.options {
|
||||
body[k] = v.clone();
|
||||
}
|
||||
let resp = self.request(reqwest::Method::POST, "/messages").json(&body).send().await?;
|
||||
let rb = self.request(reqwest::Method::POST, "/messages").json(&body);
|
||||
let resp = self.ep.with_session(rb, req.session_id.as_deref()).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(http_error(resp).await);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ pub struct ChatRequest {
|
||||
pub max_tokens: u64,
|
||||
/// Enable extended thinking / reasoning when the model supports it.
|
||||
pub reasoning: bool,
|
||||
/// Stable id of the conversation; sent to gateways that route by session
|
||||
/// (opencode: `x-opencode-session`) for routing and prompt caching.
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -84,6 +87,16 @@ impl Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Session header for gateways that need one (opencode Zen/Go reject requests without it).
|
||||
pub fn with_session(&self, rb: reqwest::RequestBuilder, session_id: Option<&str>) -> reqwest::RequestBuilder {
|
||||
match session_id {
|
||||
Some(id) if self.base_url.contains("opencode.ai") && !self.headers.keys().any(|k| k.eq_ignore_ascii_case("x-opencode-session")) => {
|
||||
rb.header("x-opencode-session", id)
|
||||
}
|
||||
_ => rb,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, mut rb: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
if !self.api_key.is_empty() {
|
||||
rb = rb.header(&self.auth_header, format!("{}{}", self.auth_prefix, self.api_key));
|
||||
@@ -99,7 +112,7 @@ impl Endpoint {
|
||||
pub fn http_client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(20))
|
||||
.user_agent(concat!("codecraft/", env!("CARGO_PKG_VERSION")))
|
||||
.user_agent(format!("codecraft/{}", crate::version::VERSION))
|
||||
.build()
|
||||
.expect("http client")
|
||||
}
|
||||
@@ -128,3 +141,27 @@ pub async fn http_error(resp: reqwest::Response) -> anyhow::Error {
|
||||
.unwrap_or_else(|| body.chars().take(500).collect());
|
||||
anyhow!("provider returned HTTP {status}: {detail}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod session_header_tests {
|
||||
use super::*;
|
||||
|
||||
fn ep(base: &str) -> Endpoint {
|
||||
Endpoint { base_url: base.into(), api_key: String::new(), auth_header: "Authorization".into(), auth_prefix: "Bearer ".into(), headers: BTreeMap::new() }
|
||||
}
|
||||
|
||||
fn header(ep: &Endpoint, id: Option<&str>) -> Option<String> {
|
||||
let rb = ep.with_session(reqwest::Client::new().post(format!("{}/chat/completions", ep.base_url)), id);
|
||||
rb.build().unwrap().headers().get("x-opencode-session").map(|v| v.to_str().unwrap().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_gets_the_session_header() {
|
||||
assert_eq!(header(&ep("https://opencode.ai/zen/go/v1"), Some("s1")).as_deref(), Some("s1"));
|
||||
assert_eq!(header(&ep("https://opencode.ai/zen/go/v1"), None), None);
|
||||
assert_eq!(header(&ep("https://api.openai.com/v1"), Some("s1")), None);
|
||||
let mut custom = ep("https://opencode.ai/zen/v1");
|
||||
custom.headers.insert("X-Opencode-Session".into(), "fixed".into());
|
||||
assert_eq!(header(&custom, Some("s1")), None, "a configured header wins");
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ impl Provider for OpenAi {
|
||||
body[k] = v.clone();
|
||||
}
|
||||
let rb = self.client.post(format!("{}/chat/completions", self.ep.base_url)).json(&body);
|
||||
let resp = self.ep.apply(rb).send().await?;
|
||||
let resp = self.ep.with_session(self.ep.apply(rb), req.session_id.as_deref()).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(http_error(resp).await);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user