Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad438a5dcb | ||
|
|
45def5c6b6 | ||
|
|
5d8455b4a0 | ||
|
|
b255c60b0b |
@@ -1,4 +1,6 @@
|
|||||||
target
|
target
|
||||||
|
models
|
||||||
|
*.cmf
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
docs
|
docs
|
||||||
|
|||||||
+15
-1
@@ -3,7 +3,21 @@
|
|||||||
All notable changes to this project are documented here.
|
All notable changes to this project are documented here.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
## [0.2.32] - 2026-07-22
|
## [0.2.36] - 2026-07-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Docker image failed to start when built fresh (`GLIBC_2.39 not found`)**: the floating `rust:1-slim` builder had drifted to Debian trixie while the runtime stayed on bookworm, so a locally built container crashed on startup. Builder and runtime are now pinned to the same Debian release (trixie).
|
||||||
|
- **Docker model downloads (curl exit code 23)**: the runtime image was missing `curl`, and the default models dir (`$HOME/.cortiq-gateway/models`) was not writable by the container user, so importing a ready `.cmf` model failed with a write error. The image now bundles `curl`, keeps all gateway state under `/app/data` (`CORTIQ_GATEWAY_HOME`), and runs as root so bind-mounted host folders (Docker Desktop on Windows/macOS) are writable; pass `--user 10001` to harden when not using host bind mounts.
|
||||||
|
- **Docker Compose persistence**: added a `cortiq-data` volume at `/app/data` so downloaded models and stats survive `docker compose up --force-recreate`.
|
||||||
|
- **Clear import errors**: a models dir that cannot be created now fails the import immediately with the path and reason instead of a cryptic curl exit code; curl exits 23 (write error) and 22 (HTTP error) are annotated with a human-readable hint in the job log.
|
||||||
|
|
||||||
|
## [0.2.35] - 2026-07-23
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Streaming Pipeline System Prompt Merging**: Fixed system prompt injection in `run_stream` so configured model system prompts and reasoning suppression directives are properly passed into streaming requests.
|
||||||
|
- **CMF Reasoning Mode Compatibility**: Fully integrated with `cortiq-engine` v0.5.17 for sampler-level logit bias reasoning token suppression and zero-token thinking bypass.
|
||||||
|
|
||||||
|
## [0.2.34] - 2026-07-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Responsive mobile admin console**: Added a hamburger navigation drawer, touch-friendly controls, phone-sized forms and cards, scrollable tables, compact toolbars, safe-area support, and mobile bottom-sheet dialogs across the admin interface.
|
- **Responsive mobile admin console**: Added a hamburger navigation drawer, touch-friendly controls, phone-sized forms and cards, scrollable tables, compact toolbars, safe-area support, and mobile bottom-sheet dialogs across the admin interface.
|
||||||
|
|||||||
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.33"
|
version = "0.2.36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cortiq-gateway"
|
name = "cortiq-gateway"
|
||||||
version = "0.2.34"
|
version = "0.2.36"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
+13
-5
@@ -1,5 +1,7 @@
|
|||||||
# ---- build stage ------------------------------------------------------------
|
# ---- build stage ------------------------------------------------------------
|
||||||
FROM rust:1-slim AS builder
|
# pin builder and runtime to the same Debian release — a floating `rust:1-slim`
|
||||||
|
# drifts to a newer glibc than the runtime image and the binary refuses to start
|
||||||
|
FROM rust:1-slim-trixie AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# the admin SPA (web/) is embedded at compile time via rust-embed, so it must be present
|
# the admin SPA (web/) is embedded at compile time via rust-embed, so it must be present
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
@@ -8,18 +10,24 @@ COPY web ./web
|
|||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# ---- runtime stage ----------------------------------------------------------
|
# ---- runtime stage ----------------------------------------------------------
|
||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
|
# curl is required at runtime — model imports download ready .cmf files with it
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& useradd -r -u 10001 cortiq \
|
&& useradd -r -u 10001 cortiq \
|
||||||
&& mkdir -p /app/config
|
&& mkdir -p /app/config /app/data/models
|
||||||
COPY --from=builder /app/target/release/cortiq-gateway /usr/local/bin/cortiq-gateway
|
COPY --from=builder /app/target/release/cortiq-gateway /usr/local/bin/cortiq-gateway
|
||||||
# ship the example as the default config so `docker run` works out of the box
|
# ship the example as the default config so `docker run` works out of the box
|
||||||
COPY config/gateway.example.toml /app/config/gateway.toml
|
COPY config/gateway.example.toml /app/config/gateway.toml
|
||||||
RUN chown -R 10001:10001 /app
|
RUN chown -R 10001:10001 /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
USER cortiq
|
# all gateway state (downloaded models, stats, secrets) lives under /app/data —
|
||||||
|
# mount a volume there to keep it across container recreates
|
||||||
|
ENV CORTIQ_GATEWAY_HOME=/app/data
|
||||||
|
# runs as root so writes to bind-mounted host folders (Docker Desktop on
|
||||||
|
# Windows/macOS) don't fail with permission errors (curl exit code 23);
|
||||||
|
# add `--user 10001` to docker run to harden when not using host bind mounts
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
ENTRYPOINT ["cortiq-gateway"]
|
ENTRYPOINT ["cortiq-gateway"]
|
||||||
CMD ["--config", "/app/config/gateway.toml"]
|
CMD ["--config", "/app/config/gateway.toml"]
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ One OpenAI-compatible endpoint → automatic model selection from your pool
|
|||||||
```bash
|
```bash
|
||||||
# Docker (GitHub Container Registry)
|
# Docker (GitHub Container Registry)
|
||||||
docker run -p 9000:9000 -e CORTIQ_ADMIN_TOKEN=secret \
|
docker run -p 9000:9000 -e CORTIQ_ADMIN_TOKEN=secret \
|
||||||
-v "$PWD/config:/app/config" ghcr.io/infosave2007/cortiq-gateway:latest
|
-v "$PWD/config:/app/config" -v cortiq-data:/app/data \
|
||||||
|
ghcr.io/infosave2007/cortiq-gateway:latest
|
||||||
# → admin console at http://localhost:9000/admin
|
# → admin console at http://localhost:9000/admin
|
||||||
|
|
||||||
# from crates.io
|
# from crates.io
|
||||||
@@ -67,7 +68,10 @@ cargo build --release # ./target/release/cortiq-gateway
|
|||||||
```
|
```
|
||||||
|
|
||||||
The image ships a default config and runs out of the box; mount `/app/config` (with a
|
The image ships a default config and runs out of the box; mount `/app/config` (with a
|
||||||
`gateway.toml`) to use your own. Tags: `:latest`, `:X.Y.Z`, `:edge` (linux/amd64 + arm64).
|
`gateway.toml`) to use your own. Downloaded models and stats live in `/app/data` — mount
|
||||||
|
a volume there so they survive container recreates (a host folder works too, e.g.
|
||||||
|
`-v "$PWD/models:/app/data/models"`). Tags: `:latest`, `:X.Y.Z`, `:edge`
|
||||||
|
(linux/amd64 + arm64).
|
||||||
|
|
||||||
**Docker Compose** — gateway + a local Ollama model in one command:
|
**Docker Compose** — gateway + a local Ollama model in one command:
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ services:
|
|||||||
CORTIQ_ROUTER_KEY: ${CORTIQ_ROUTER_KEY:-}
|
CORTIQ_ROUTER_KEY: ${CORTIQ_ROUTER_KEY:-}
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/gateway.compose.toml:/app/config/gateway.toml:ro
|
- ./config/gateway.compose.toml:/app/config/gateway.toml:ro
|
||||||
|
# downloaded .cmf models + stats persist here across recreates; swap the
|
||||||
|
# named volume for a host folder (e.g. ./models:/app/data/models) if you
|
||||||
|
# want the files visible next to the compose file
|
||||||
|
- cortiq-data:/app/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- ollama
|
- ollama
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -30,4 +34,5 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
cortiq-data:
|
||||||
ollama:
|
ollama:
|
||||||
|
|||||||
+14
-2
@@ -406,7 +406,13 @@ async fn download_cmf_repo(
|
|||||||
}
|
}
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
cleanup_output(&output_abs);
|
cleanup_output(&output_abs);
|
||||||
store.push_line(&id, format!("✗ download failed: exit code {:?}", status.code()));
|
// curl 23 = write error: almost always a non-writable models dir
|
||||||
|
let hint = match status.code() {
|
||||||
|
Some(23) => " (write error — is the models dir writable?)",
|
||||||
|
Some(22) => " (HTTP error — repo/file not found or auth required)",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
store.push_line(&id, format!("✗ download failed: exit code {:?}{hint}", status.code()));
|
||||||
store.finish(&id, false);
|
store.finish(&id, false);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -471,7 +477,13 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = std::fs::create_dir_all(&cfg.models_dir);
|
std::fs::create_dir_all(&cfg.models_dir).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"cannot create models dir {}: {e} — check that the gateway has write \
|
||||||
|
access (in Docker, mount a writable volume at the models path)",
|
||||||
|
cfg.models_dir
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let base = if p.name.trim().is_empty() {
|
let base = if p.name.trim().is_empty() {
|
||||||
sanitize(p.repo.rsplit('/').next().unwrap_or(&p.repo))
|
sanitize(p.repo.rsplit('/').next().unwrap_or(&p.repo))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+7
-1
@@ -99,6 +99,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let mut app = Router::new()
|
let mut app = Router::new()
|
||||||
.route("/healthz", get(|| async { "ok" }))
|
.route("/healthz", get(|| async { "ok" }))
|
||||||
|
.route("/health", get(|| async { "ok" }))
|
||||||
.route("/readyz", get(|| async { "ready" }))
|
.route("/readyz", get(|| async { "ready" }))
|
||||||
.route("/metrics", get(admin::metrics))
|
.route("/metrics", get(admin::metrics))
|
||||||
.merge(protocols::build_router());
|
.merge(protocols::build_router());
|
||||||
@@ -109,7 +110,12 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.fallback(admin::assets::fallback);
|
.fallback(admin::assets::fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
let app = app.with_state(state);
|
let cors = tower_http::cors::CorsLayer::new()
|
||||||
|
.allow_origin(tower_http::cors::Any)
|
||||||
|
.allow_methods(tower_http::cors::Any)
|
||||||
|
.allow_headers(tower_http::cors::Any);
|
||||||
|
|
||||||
|
let app = app.with_state(state).layer(cors);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&listen).await?;
|
let listener = tokio::net::TcpListener::bind(&listen).await?;
|
||||||
tracing::info!("cortiq-gateway listening on {listen}");
|
tracing::info!("cortiq-gateway listening on {listen}");
|
||||||
|
|||||||
@@ -487,6 +487,20 @@ impl Pipeline {
|
|||||||
if req_call.params.think_budget.is_none() {
|
if req_call.params.think_budget.is_none() {
|
||||||
req_call.params.think_budget = m_cfg.think_budget;
|
req_call.params.think_budget = m_cfg.think_budget;
|
||||||
}
|
}
|
||||||
|
if let Some(sys) = &m_cfg.system_prompt {
|
||||||
|
if !sys.trim().is_empty()
|
||||||
|
&& !req_call.messages.iter().any(|m| m.role == "system")
|
||||||
|
{
|
||||||
|
req_call.messages.insert(
|
||||||
|
0,
|
||||||
|
Message {
|
||||||
|
role: "system".into(),
|
||||||
|
content: sys.clone(),
|
||||||
|
tool_calls: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Some(s_cfg) = live.cfg.cmf.servers.iter().find(|s| s.id == model_id) {
|
} else if let Some(s_cfg) = live.cfg.cmf.servers.iter().find(|s| s.id == model_id) {
|
||||||
if req_call.params.temperature.is_none() {
|
if req_call.params.temperature.is_none() {
|
||||||
req_call.params.temperature = s_cfg.temperature;
|
req_call.params.temperature = s_cfg.temperature;
|
||||||
@@ -500,6 +514,20 @@ impl Pipeline {
|
|||||||
if req_call.params.think_budget.is_none() {
|
if req_call.params.think_budget.is_none() {
|
||||||
req_call.params.think_budget = s_cfg.think_budget;
|
req_call.params.think_budget = s_cfg.think_budget;
|
||||||
}
|
}
|
||||||
|
if let Some(sys) = &s_cfg.system_prompt {
|
||||||
|
if !sys.trim().is_empty()
|
||||||
|
&& !req_call.messages.iter().any(|m| m.role == "system")
|
||||||
|
{
|
||||||
|
req_call.messages.insert(
|
||||||
|
0,
|
||||||
|
Message {
|
||||||
|
role: "system".into(),
|
||||||
|
content: sys.clone(),
|
||||||
|
tool_calls: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match provider.chat_stream(req_call).await {
|
match provider.chat_stream(req_call).await {
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ impl OpenAiProvider {
|
|||||||
if let Some(m) = req.params.max_tokens {
|
if let Some(m) = req.params.max_tokens {
|
||||||
body["max_tokens"] = m.into();
|
body["max_tokens"] = m.into();
|
||||||
}
|
}
|
||||||
|
eprintln!("[to_wire] think_budget={:?}", req.params.think_budget);
|
||||||
if let Some(tb) = req.params.think_budget {
|
if let Some(tb) = req.params.think_budget {
|
||||||
body["think_budget"] = tb.into();
|
body["think_budget"] = tb.into();
|
||||||
body["reasoning_effort"] = tb.into();
|
body["reasoning_effort"] = tb.into();
|
||||||
// cortiq serve uses enable_thinking (bool): 0 = off, >0 = on
|
// cortiq serve uses enable_thinking (bool): 0 = off, >0 = on
|
||||||
body["enable_thinking"] = (tb > 0).into();
|
body["enable_thinking"] = (tb > 0).into();
|
||||||
|
eprintln!("[to_wire] SET enable_thinking={}", tb > 0);
|
||||||
}
|
}
|
||||||
if !req.tools.is_empty() {
|
if !req.tools.is_empty() {
|
||||||
body["tools"] = serde_json::Value::Array(req.tools.clone());
|
body["tools"] = serde_json::Value::Array(req.tools.clone());
|
||||||
@@ -61,6 +63,10 @@ impl OpenAiProvider {
|
|||||||
for (k, v) in &req.params.passthrough {
|
for (k, v) in &req.params.passthrough {
|
||||||
body[k] = v.clone();
|
body[k] = v.clone();
|
||||||
}
|
}
|
||||||
|
eprintln!(
|
||||||
|
"[to_wire] enable_thinking={:?}",
|
||||||
|
body.get("enable_thinking")
|
||||||
|
);
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user