Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac72167ea | ||
|
|
3575bf7e60 | ||
|
|
96af5fca98 | ||
|
|
b867a6276e | ||
|
|
c18ddb5475 | ||
|
|
ef9b3738fd | ||
|
|
924ec403bd | ||
|
|
0c91f09113 | ||
|
|
45deb6645b | ||
|
|
243d8f4c7d | ||
|
|
4f72711a1e | ||
|
|
7855472499 | ||
|
|
a6ed974ece | ||
|
|
4c11b97197 | ||
|
|
87f00ee502 | ||
|
|
4fa1b8ea22 | ||
|
|
357441e0da | ||
|
|
9d2181e82b | ||
|
|
30378f7b6a | ||
|
|
b4f78bd192 | ||
|
|
3c90dad9b9 | ||
|
|
25f93d1a27 | ||
|
|
0f89cdaa07 | ||
|
|
d211eb6150 | ||
|
|
04580196ad | ||
|
|
7a3d6a7bb4 | ||
|
|
07d011263d | ||
|
|
0040efc948 | ||
|
|
16a3e209b6 | ||
|
|
c19edee7bb | ||
|
|
7af90a5eef | ||
|
|
8d7e5fe653 | ||
|
|
2605eccb64 | ||
|
|
1317b8024b | ||
|
|
9242806795 | ||
|
|
6714c4f556 | ||
|
|
a5966dd479 | ||
|
|
e015555dff | ||
|
|
f00bf8355a | ||
|
|
ddca3a8363 | ||
|
|
a67564bc24 | ||
|
|
7dbc39c6a6 | ||
|
|
b6fdf23aa6 | ||
|
|
220df29888 | ||
|
|
74d0295181 | ||
|
|
73a2bd7b42 | ||
|
|
e97fcddf87 | ||
|
|
51487e6329 |
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "cortiq-gateway"
|
||||
version = "0.2.33"
|
||||
version = "0.2.32"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cortiq-gateway"
|
||||
version = "0.2.34"
|
||||
version = "0.2.32"
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
license = "Apache-2.0"
|
||||
|
||||
+4
-99
@@ -467,111 +467,16 @@ async fn provider_models(
|
||||
}
|
||||
// OpenAI + Anthropic both return { "data": [ { "id": ... } ] }.
|
||||
let v: Value = serde_json::from_str(&body).unwrap_or(json!({}));
|
||||
let mut models: Vec<Value> = v["data"]
|
||||
let mut models: Vec<String> = v["data"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|m| {
|
||||
let id = m["id"].as_str()?.to_string();
|
||||
let mut obj = json!({ "id": id });
|
||||
let mut caps = vec![];
|
||||
if let Some(pricing) = m.get("pricing").filter(|v| !v.is_null()) {
|
||||
if let Some(prompt) = pricing
|
||||
.get("prompt")
|
||||
.and_then(|s| s.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
{
|
||||
obj["price_in"] = json!(prompt * 1_000_000.0);
|
||||
}
|
||||
if let Some(comp) = pricing
|
||||
.get("completion")
|
||||
.and_then(|s| s.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
{
|
||||
obj["price_out"] = json!(comp * 1_000_000.0);
|
||||
}
|
||||
if pricing
|
||||
.get("input_cache_read")
|
||||
.filter(|v| !v.is_null())
|
||||
.is_some()
|
||||
|| pricing
|
||||
.get("input_cache_write")
|
||||
.filter(|v| !v.is_null())
|
||||
.is_some()
|
||||
{
|
||||
caps.push("caching");
|
||||
}
|
||||
}
|
||||
if let Some(params) = m.get("supported_parameters").and_then(|p| p.as_array()) {
|
||||
let has_tools = params.iter().any(|p| {
|
||||
p.as_str() == Some("tools") || p.as_str() == Some("tool_choice")
|
||||
});
|
||||
if has_tools {
|
||||
caps.push("tools");
|
||||
}
|
||||
let has_json = params.iter().any(|p| p.as_str() == Some("response_format"));
|
||||
if has_json {
|
||||
caps.push("json");
|
||||
}
|
||||
let has_reasoning = params.iter().any(|p| {
|
||||
p.as_str() == Some("reasoning")
|
||||
|| p.as_str() == Some("include_reasoning")
|
||||
});
|
||||
if has_reasoning || m.get("reasoning").filter(|v| !v.is_null()).is_some() {
|
||||
caps.push("reasoning");
|
||||
}
|
||||
}
|
||||
if let Some(arch) = m.get("architecture").filter(|v| !v.is_null()) {
|
||||
if let Some(inputs) =
|
||||
arch.get("input_modalities").and_then(|i| i.as_array())
|
||||
{
|
||||
if inputs.iter().any(|i| i.as_str() == Some("image")) {
|
||||
caps.push("vision");
|
||||
}
|
||||
if inputs.iter().any(|i| i.as_str() == Some("audio")) {
|
||||
caps.push("audio");
|
||||
}
|
||||
}
|
||||
}
|
||||
if !caps.is_empty() {
|
||||
obj["caps"] = json!(caps);
|
||||
}
|
||||
Some(obj)
|
||||
})
|
||||
.filter_map(|m| m["id"].as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
models.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
|
||||
|
||||
let mut resp = json!({ "ok": true, "count": models.len(), "models": models });
|
||||
|
||||
if b.provider == "openrouter" {
|
||||
if let Some(k) = key {
|
||||
if let Ok(cred_resp) = client
|
||||
.get("https://openrouter.ai/api/v1/credits")
|
||||
.header("authorization", format!("Bearer {k}"))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
if let Ok(cred_body) = cred_resp.text().await {
|
||||
let cred_v: Value = serde_json::from_str(&cred_body).unwrap_or(json!({}));
|
||||
if let Some(data) = cred_v.get("data") {
|
||||
let total = data
|
||||
.get("total_credits")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
let usage = data
|
||||
.get("total_usage")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
resp["balance"] = json!(total - usage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok(resp)
|
||||
models.sort();
|
||||
ok(json!({ "ok": true, "count": models.len(), "models": models }))
|
||||
}
|
||||
|
||||
async fn meta() -> ApiResult {
|
||||
|
||||
+4
-8
@@ -324,7 +324,7 @@ async fn download_cmf_repo(
|
||||
let cmf_path = match cmf_rel_path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let repo_name = repo.rsplit('/').next().unwrap_or(&repo);
|
||||
let repo_name = repo.split('/').last().unwrap_or(&repo);
|
||||
format!("{repo_name}.cmf")
|
||||
}
|
||||
};
|
||||
@@ -491,13 +491,9 @@ pub fn start_import(store: Arc<JobStore>, cfg: &CmfCfg, p: ImportParams) -> Resu
|
||||
let r = p.repo.to_lowercase();
|
||||
if r.contains("2bit") || r.contains("q1t") {
|
||||
"Q1T".to_string()
|
||||
} else if r.contains("1.7bcmf")
|
||||
|| r.contains("27bcmf")
|
||||
|| r.contains("cmf")
|
||||
|| p.quant.is_empty()
|
||||
|| p.quant == "auto"
|
||||
|| p.quant == "Q8_2F"
|
||||
{
|
||||
} else if r.contains("1.7bcmf") || r.contains("27bcmf") || r.contains("cmf") {
|
||||
"Q1".to_string()
|
||||
} else if p.quant.is_empty() || p.quant == "auto" || p.quant == "Q8_2F" {
|
||||
"Q1".to_string()
|
||||
} else {
|
||||
p.quant.clone()
|
||||
|
||||
+1
-31
@@ -472,37 +472,7 @@ impl Pipeline {
|
||||
model_id,
|
||||
route_source
|
||||
);
|
||||
// Merge per-model/server config (same as run_inner).
|
||||
let mut req_call = req.clone();
|
||||
if let Some(m_cfg) = live.cfg.models.iter().find(|m| m.id == model_id) {
|
||||
if req_call.params.temperature.is_none() {
|
||||
req_call.params.temperature = m_cfg.temperature;
|
||||
}
|
||||
if req_call.params.top_p.is_none() {
|
||||
req_call.params.top_p = m_cfg.top_p;
|
||||
}
|
||||
if req_call.params.max_tokens.is_none() {
|
||||
req_call.params.max_tokens = m_cfg.max_tokens;
|
||||
}
|
||||
if req_call.params.think_budget.is_none() {
|
||||
req_call.params.think_budget = m_cfg.think_budget;
|
||||
}
|
||||
} else if let Some(s_cfg) = live.cfg.cmf.servers.iter().find(|s| s.id == model_id) {
|
||||
if req_call.params.temperature.is_none() {
|
||||
req_call.params.temperature = s_cfg.temperature;
|
||||
}
|
||||
if req_call.params.top_p.is_none() {
|
||||
req_call.params.top_p = s_cfg.top_p;
|
||||
}
|
||||
if req_call.params.max_tokens.is_none() {
|
||||
req_call.params.max_tokens = s_cfg.max_tokens;
|
||||
}
|
||||
if req_call.params.think_budget.is_none() {
|
||||
req_call.params.think_budget = s_cfg.think_budget;
|
||||
}
|
||||
}
|
||||
|
||||
match provider.chat_stream(req_call).await {
|
||||
match provider.chat_stream(req.clone()).await {
|
||||
Ok(stream) => {
|
||||
let info = RouteInfo {
|
||||
task_label: decision.task_label.clone(),
|
||||
|
||||
@@ -51,8 +51,6 @@ impl OpenAiProvider {
|
||||
if let Some(tb) = req.params.think_budget {
|
||||
body["think_budget"] = tb.into();
|
||||
body["reasoning_effort"] = tb.into();
|
||||
// cortiq serve uses enable_thinking (bool): 0 = off, >0 = on
|
||||
body["enable_thinking"] = (tb > 0).into();
|
||||
}
|
||||
if !req.tools.is_empty() {
|
||||
body["tools"] = serde_json::Value::Array(req.tools.clone());
|
||||
|
||||
Vendored
+1
-1
@@ -19,6 +19,6 @@
|
||||
<noscript>This console requires JavaScript.</noscript>
|
||||
<div id="app"><div class="boot">Loading…</div></div>
|
||||
<div id="toasts" class="toasts"></div>
|
||||
<script type="module" src="/admin/js/app.js?v=3"></script>
|
||||
<script type="module" src="/admin/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Vendored
+1
-1
@@ -4,7 +4,7 @@ import { t, getLang, setLang, onLangChange, LANGS } from "./i18n.js";
|
||||
import { api, getToken, setToken, setUnauthorizedHandler } from "./api.js";
|
||||
import { renderLogin } from "./views/login.js";
|
||||
import { renderDashboard } from "./views/dashboard.js";
|
||||
import { renderModels } from "./views/models.js?v=3";
|
||||
import { renderModels } from "./views/models.js";
|
||||
import { renderImport } from "./views/import.js";
|
||||
import { renderRouting } from "./views/routing.js";
|
||||
import { renderProtocols } from "./views/protocols.js";
|
||||
|
||||
Vendored
+18
-64
@@ -51,16 +51,7 @@ function modelForm(meta, existing) {
|
||||
const tierSel = h("select", {}, ...(meta.cost_tiers || ["cheap"]).map((c) => opt(c, c, c === m.cost_tier)));
|
||||
const priceInIn = h("input", { type: "number", step: "0.01", value: m.price_in ?? 0 });
|
||||
const priceOutIn = h("input", { type: "number", step: "0.01", value: m.price_out ?? 0 });
|
||||
const CAP_EMOJIS = {
|
||||
tools: "🔧 tools",
|
||||
vision: "👁️ vision",
|
||||
audio: "🎙️ audio",
|
||||
reasoning: "🧠 reasoning",
|
||||
json: "📄 json",
|
||||
caching: "⚡ caching"
|
||||
};
|
||||
|
||||
const capInputs = (meta.caps || Object.keys(CAP_EMOJIS)).map((c) => {
|
||||
const capInputs = (meta.caps || ["tools", "vision"]).map((c) => {
|
||||
const cb = h("input", { type: "checkbox", checked: (m.caps || []).includes(c) ? true : null });
|
||||
return { c, cb };
|
||||
});
|
||||
@@ -79,55 +70,25 @@ function modelForm(meta, existing) {
|
||||
capInputs.forEach(({ c, cb }) => { cb.checked = d.caps.includes(c); });
|
||||
keyIn.placeholder = d.needsKey ? t("models.form.apiKeyPlaceholder") : t("models.form.apiKeyNotNeeded");
|
||||
mount(keyStatus, "");
|
||||
// Automatically pre-fetch models (especially useful for OpenRouter which doesn't need a key)
|
||||
testKey(true);
|
||||
}
|
||||
providerSel.addEventListener("change", applyDefaults);
|
||||
if (!existing) applyDefaults();
|
||||
|
||||
let cachedModels = [];
|
||||
|
||||
const autofillData = () => {
|
||||
console.log("autofillData triggered. modelIn.value:", modelIn.value);
|
||||
console.log("cachedModels length:", cachedModels.length);
|
||||
if (!existing && (!idIn.value || idIn._auto)) {
|
||||
idIn.value = slugId(modelIn.value);
|
||||
idIn._auto = true;
|
||||
console.log("auto-filled idIn:", idIn.value);
|
||||
}
|
||||
const found = cachedModels.find((m) => (m.id || m) === modelIn.value);
|
||||
console.log("found model:", found);
|
||||
if (found) {
|
||||
if (found.price_in !== undefined) {
|
||||
priceInIn.value = parseFloat(Number(found.price_in).toFixed(4));
|
||||
priceOutIn.value = parseFloat(Number(found.price_out).toFixed(4));
|
||||
console.log("Updated prices:", priceInIn.value, priceOutIn.value);
|
||||
}
|
||||
if (found.caps && Array.isArray(found.caps)) {
|
||||
capInputs.forEach(({ c, cb }) => {
|
||||
cb.checked = found.caps.includes(c);
|
||||
});
|
||||
console.log("Updated caps:", found.caps);
|
||||
}
|
||||
}
|
||||
};
|
||||
modelIn.addEventListener("input", autofillData);
|
||||
modelIn.addEventListener("change", autofillData);
|
||||
|
||||
// Auto-fill the id from the model name (until the user edits id by hand).
|
||||
if (!existing) {
|
||||
modelIn.addEventListener("input", () => {
|
||||
if (!idIn.value || idIn._auto) {
|
||||
idIn.value = slugId(modelIn.value);
|
||||
idIn._auto = true;
|
||||
}
|
||||
});
|
||||
idIn.addEventListener("input", () => { idIn._auto = false; });
|
||||
} else {
|
||||
console.log("Editing existing model, fetching testKey(true)");
|
||||
testKey(true);
|
||||
}
|
||||
|
||||
async function testKey(silent = false) {
|
||||
if (!baseUrlIn.value.trim()) {
|
||||
if (!silent) mount(keyStatus, h("span", { style: "color:var(--bad)" }, t("models.form.baseUrl") + "?"));
|
||||
return;
|
||||
}
|
||||
async function testKey() {
|
||||
if (!baseUrlIn.value.trim()) { mount(keyStatus, h("span", { style: "color:var(--bad)" }, t("models.form.baseUrl") + "?")); return; }
|
||||
testBtn.disabled = true;
|
||||
if (!silent) mount(keyStatus, t("models.form.testing"));
|
||||
mount(keyStatus, t("models.form.testing"));
|
||||
try {
|
||||
const r = await api.providerModels({
|
||||
provider: providerSel.value,
|
||||
@@ -136,25 +97,18 @@ function modelForm(meta, existing) {
|
||||
api_key_env: envFor(),
|
||||
});
|
||||
if (r.ok) {
|
||||
cachedModels = r.models || [];
|
||||
dl.replaceChildren(...cachedModels.map((mm) => h("option", { value: mm.id || mm })));
|
||||
|
||||
let msg = "✓ " + t("models.form.foundModels", { n: r.count });
|
||||
if (r.balance !== undefined) {
|
||||
msg += ` (Balance: $${r.balance.toFixed(4)})`;
|
||||
}
|
||||
mount(keyStatus, h("span", { style: "color:var(--good)" }, msg));
|
||||
if (!silent) modelIn.focus();
|
||||
dl.replaceChildren(...(r.models || []).map((mm) => h("option", { value: mm })));
|
||||
mount(keyStatus, h("span", { style: "color:var(--good)" }, "✓ " + t("models.form.foundModels", { n: r.count })));
|
||||
modelIn.focus();
|
||||
} else {
|
||||
if (!silent) mount(keyStatus, h("span", { style: "color:var(--bad)" }, "✗ " + (r.error || t("common.error"))));
|
||||
mount(keyStatus, h("span", { style: "color:var(--bad)" }, "✗ " + (r.error || t("common.error"))));
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) mount(keyStatus, h("span", { style: "color:var(--bad)" }, "✗ " + String(e.message || e)));
|
||||
mount(keyStatus, h("span", { style: "color:var(--bad)" }, "✗ " + String(e.message || e)));
|
||||
}
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
testBtn.addEventListener("click", () => testKey(false));
|
||||
keyIn.addEventListener("change", () => testKey(true));
|
||||
testBtn.addEventListener("click", testKey);
|
||||
|
||||
const keyHint =
|
||||
existing && existing.key_source === "store" ? t("models.form.keyStored")
|
||||
@@ -201,7 +155,7 @@ function modelForm(meta, existing) {
|
||||
dl,
|
||||
h("div", { class: "row" }, field(t("models.form.kind"), kindSel), field(t("models.form.costTier"), tierSel)),
|
||||
h("div", { class: "row" }, field(t("models.form.priceIn"), priceInIn), field(t("models.form.priceOut"), priceOutIn)),
|
||||
field(t("models.form.caps"), h("div", { class: "flex wrap", style: "gap:10px" }, ...capInputs.map(({ c, cb }) => h("label", { class: "check", style: "margin:0" }, cb, CAP_EMOJIS[c] || c)))),
|
||||
field(t("models.form.caps"), h("div", { class: "flex wrap" }, ...capInputs.map(({ c, cb }) => h("label", { class: "check" }, cb, c)))),
|
||||
advParams
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user