feat(keys): one-paste harness setup — Claude Code, Codex, OpenCode, Continue, Cline

The "connect your app" card in the API-keys view now leads with auto-setup
scripts for popular coding harnesses, each pre-filled with this gateway's URL
and the freshly created key:

  Claude Code — env exports (the gateway speaks Anthropic /v1/messages
      natively; the protocol is now ON by default, and when a config has it
      off the tab shows a warning with a one-click Enable button)
  Codex — appends a [model_providers.cortiq] block to ~/.codex/config.toml
  OpenCode — writes the provider into opencode.json
  Continue — appends the model to ~/.continue/config.yaml
  Cline — the exact values to paste into the extension settings

The gateway also detects which harnesses are already present on its machine
(binaries across PATH + common install dirs, config files under home) via
GET /admin/api/harnesses: installed tabs get a ✓, and the status line under
the snippet says "installed" or "already configured for this gateway" when
the harness config mentions cortiq. Works on Linux/macOS/Windows.

curl / python / javascript SDK snippets stay as before. 16 new strings in
all 7 languages.
This commit is contained in:
infosave2007
2026-08-07 19:26:54 +03:00
parent 54b73f1365
commit 1bba427042
13 changed files with 295 additions and 8 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ openai_chat = true
openai_completions = true
openai_embeddings = true
openai_models = true
anthropic_messages = false
anthropic_messages = true
mcp = false
native_passthrough = true
+79
View File
@@ -160,6 +160,7 @@ pub fn api_routes(admin_token: String) -> Router<SharedState> {
"/admin/api/protocols",
get(get_protocols).put(put_protocols),
)
.route("/admin/api/harnesses", get(get_harnesses))
.route("/admin/api/settings", get(get_settings).put(put_settings))
.route("/admin/api/keys", get(list_keys).post(create_key))
.route("/admin/api/keys/:key", axum::routing::delete(delete_key))
@@ -989,6 +990,84 @@ async fn put_routing(State(state): State<SharedState>, Json(body): Json<RoutingB
ok(json!({ "ok": true }))
}
// ──────────────────────────── harness detection ────────────────────────────
// Which coding harnesses are present on the machine the gateway runs on, so the
// API-keys view can mark tabs "installed" / "already configured for cortiq".
// Detection is best-effort: binaries in PATH + common install dirs, config
// files under the user's home. Works on Linux/macOS/Windows.
fn harness_home() -> Option<std::path::PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(std::path::PathBuf::from)
}
fn find_bin(name: &str) -> bool {
let exts: &[&str] = if cfg!(windows) {
&[".exe", ".cmd", ".bat", ""]
} else {
&[""]
};
let mut dirs: Vec<std::path::PathBuf> = std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).collect())
.unwrap_or_default();
if let Some(home) = harness_home() {
// GUI-launched processes often miss the shell PATH — add usual suspects
for extra in [
".local/bin",
".cargo/bin",
"bin",
".bun/bin",
".npm-global/bin",
"AppData/Roaming/npm",
] {
dirs.push(home.join(extra));
}
}
dirs.extend(["/usr/local/bin", "/opt/homebrew/bin"].map(std::path::PathBuf::from));
dirs.iter()
.any(|d| exts.iter().any(|e| d.join(format!("{name}{e}")).is_file()))
}
/// Does the harness config mention cortiq (i.e. was it pointed at this gateway)?
fn config_mentions_cortiq(rel: &str) -> bool {
harness_home()
.map(|h| h.join(rel))
.and_then(|p| std::fs::read_to_string(p).ok())
.map(|c| c.to_lowercase().contains("cortiq"))
.unwrap_or(false)
}
async fn get_harnesses() -> ApiResult {
let harnesses = tokio::task::spawn_blocking(|| {
let vscode_ext_glob = |marker: &str| -> bool {
let Some(home) = harness_home() else {
return false;
};
for extdir in [".vscode/extensions", ".vscode-server/extensions"] {
if let Ok(rd) = std::fs::read_dir(home.join(extdir)) {
for e in rd.flatten() {
if e.file_name().to_string_lossy().starts_with(marker) {
return true;
}
}
}
}
false
};
json!({
"Claude Code": { "installed": find_bin("claude"), "configured": serde_json::Value::Null },
"Codex": { "installed": find_bin("codex"), "configured": config_mentions_cortiq(".codex/config.toml") },
"OpenCode": { "installed": find_bin("opencode"), "configured": config_mentions_cortiq(".config/opencode/opencode.json") },
"Continue": { "installed": harness_home().map(|h| h.join(".continue").is_dir()).unwrap_or(false), "configured": config_mentions_cortiq(".continue/config.yaml") },
"Cline": { "installed": vscode_ext_glob("saoudrizwan.claude-dev"), "configured": serde_json::Value::Null },
})
})
.await
.unwrap_or_else(|_| json!({}));
ok(harnesses)
}
// ───────────────────────────── protocols ───────────────────────────────────
async fn get_protocols(State(state): State<SharedState>) -> ApiResult {
+3 -1
View File
@@ -757,7 +757,9 @@ impl Default for Config {
openai_completions: true,
openai_embeddings: true,
openai_models: true,
anthropic_messages: false,
// on by default: the API-keys view ships a Claude Code
// auto-setup snippet that targets /v1/messages
anthropic_messages: true,
mcp: false,
native_passthrough: true,
},
+4
View File
@@ -588,6 +588,10 @@ body.modal-open { overflow: hidden; }
/* Money: 4 decimals, last two de-emphasized */
.money-tail { font-size: .72em; opacity: .75; margin-left: 1px; }
.seg { flex-wrap: wrap; }
.ok { color: var(--good); }
/* Benchmark view */
@keyframes bm-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
tbody tr:last-child { animation: bm-in .25s ease; }
+1
View File
@@ -73,6 +73,7 @@ export const api = {
// protocols
getProtocols: () => j("GET", "/protocols"),
harnesses: () => j("GET", "/harnesses"),
putProtocols: (b) => j("PUT", "/protocols", b),
// settings
+14
View File
@@ -602,4 +602,18 @@ export default {
"bm.spent": "ausgegeben",
"bm.verdict": "🏁 Fertig: Antworten {p}, ausgegeben {s}",
"bm.clearHistory": "Alle löschen",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — ins Terminal einfügen (das Gateway spricht das Anthropic-Protokoll nativ):",
"keys.h.codex": "Codex — hängt den Provider an ~/.codex/config.toml an und startet:",
"keys.h.opencode": "OpenCode — schreibt ~/.config/opencode/opencode.json (überschreibt!) und startet:",
"keys.h.continue": "Continue (VS Code / JetBrains) — hängt das Modell an ~/.continue/config.yaml an:",
"keys.h.cline1": "Cline (VS Code) — in den Erweiterungseinstellungen wählen:",
"keys.h.cline2": "Einstellungen → API Provider → OpenAI Compatible, dann die Werte oben einfügen.",
"keys.h.note": "cortiq-auto = smartes Routing: der Router wählt das Modell pro Anfrage. Für ein festes Modell den exakten Namen angeben. Unter Windows die Skripte in Git Bash / WSL ausführen oder dieselben Variablen über die Systemumgebung setzen.",
"keys.h.protoOff": "Das Anthropic-Protokoll (/v1/messages) ist deaktiviert — Claude Code kann sich nicht verbinden.",
"keys.h.enableProto": "Aktivieren",
"keys.h.st.installed": "Auf der Gateway-Maschine installiert",
"keys.h.st.configured": "Bereits für dieses Gateway konfiguriert",
"keys.h.st.notFound": "Auf der Gateway-Maschine nicht gefunden",
};
+14
View File
@@ -595,4 +595,18 @@ export default {
"bm.spent": "spent",
"bm.verdict": "🏁 Done: answers {p}, spent {s}",
"bm.clearHistory": "Clear all",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — paste into the terminal (the gateway speaks the Anthropic protocol natively):",
"keys.h.codex": "Codex — appends the provider to ~/.codex/config.toml and starts:",
"keys.h.opencode": "OpenCode — writes ~/.config/opencode/opencode.json (overwrites!) and starts:",
"keys.h.continue": "Continue (VS Code / JetBrains) — appends the model to ~/.continue/config.yaml:",
"keys.h.cline1": "Cline (VS Code) — in extension settings pick:",
"keys.h.cline2": "Settings → API Provider → OpenAI Compatible, then paste the values above.",
"keys.h.note": "cortiq-auto = smart routing: the router picks the model per request. Use an exact model name to pin one. On Windows run the scripts in Git Bash / WSL, or set the same variables via the system environment settings.",
"keys.h.protoOff": "The Anthropic protocol (/v1/messages) is disabled — Claude Code will not connect.",
"keys.h.enableProto": "Enable",
"keys.h.st.installed": "Installed on the gateway machine",
"keys.h.st.configured": "Already configured for this gateway",
"keys.h.st.notFound": "Not found on the gateway machine",
};
+14
View File
@@ -602,4 +602,18 @@ export default {
"bm.spent": "gastado",
"bm.verdict": "🏁 Listo: respuestas {p}, gastado {s}",
"bm.clearHistory": "Borrar todo",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — pega en la terminal (la pasarela habla el protocolo Anthropic de forma nativa):",
"keys.h.codex": "Codex — añade el proveedor a ~/.codex/config.toml y arranca:",
"keys.h.opencode": "OpenCode — escribe ~/.config/opencode/opencode.json (¡sobrescribe!) y arranca:",
"keys.h.continue": "Continue (VS Code / JetBrains) — añade el modelo a ~/.continue/config.yaml:",
"keys.h.cline1": "Cline (VS Code) — en los ajustes de la extensión elige:",
"keys.h.cline2": "Ajustes → API Provider → OpenAI Compatible, luego pega los valores de arriba.",
"keys.h.note": "cortiq-auto = enrutado inteligente: el router elige el modelo en cada petición. Para fijar uno, indica su nombre exacto. En Windows ejecuta los scripts en Git Bash / WSL o define las mismas variables en el entorno del sistema.",
"keys.h.protoOff": "El protocolo Anthropic (/v1/messages) está desactivado — Claude Code no podrá conectarse.",
"keys.h.enableProto": "Activar",
"keys.h.st.installed": "Instalado en la máquina de la pasarela",
"keys.h.st.configured": "Ya configurado para esta pasarela",
"keys.h.st.notFound": "No encontrado en la máquina de la pasarela",
};
+14
View File
@@ -602,4 +602,18 @@ export default {
"bm.spent": "dépensé",
"bm.verdict": "🏁 Terminé : réponses {p}, dépensé {s}",
"bm.clearHistory": "Tout effacer",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — collez dans le terminal (la passerelle parle nativement le protocole Anthropic) :",
"keys.h.codex": "Codex — ajoute le fournisseur à ~/.codex/config.toml et démarre :",
"keys.h.opencode": "OpenCode — écrit ~/.config/opencode/opencode.json (écrase !) et démarre :",
"keys.h.continue": "Continue (VS Code / JetBrains) — ajoute le modèle à ~/.continue/config.yaml :",
"keys.h.cline1": "Cline (VS Code) — dans les réglages de l'extension, choisissez :",
"keys.h.cline2": "Réglages → API Provider → OpenAI Compatible, puis collez les valeurs ci-dessus.",
"keys.h.note": "cortiq-auto = routage intelligent : le routeur choisit le modèle à chaque requête. Pour en fixer un, indiquez son nom exact. Sous Windows, lancez les scripts dans Git Bash / WSL ou définissez les mêmes variables via l'environnement système.",
"keys.h.protoOff": "Le protocole Anthropic (/v1/messages) est désactivé — Claude Code ne pourra pas se connecter.",
"keys.h.enableProto": "Activer",
"keys.h.st.installed": "Installé sur la machine de la passerelle",
"keys.h.st.configured": "Déjà configuré pour cette passerelle",
"keys.h.st.notFound": "Introuvable sur la machine de la passerelle",
};
+14
View File
@@ -594,4 +594,18 @@ export default {
"bm.spent": "потрачено",
"bm.verdict": "🏁 Готово: ответы {p}, потрачено {s}",
"bm.clearHistory": "Очистить всё",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — вставьте в терминал (шлюз говорит на протоколе Anthropic нативно):",
"keys.h.codex": "Codex — добавит провайдера в ~/.codex/config.toml и запустит:",
"keys.h.opencode": "OpenCode — запишет ~/.config/opencode/opencode.json (перезапишет!) и запустит:",
"keys.h.continue": "Continue (VS Code / JetBrains) — добавит модель в ~/.continue/config.yaml:",
"keys.h.cline1": "Cline (VS Code) — в настройках расширения укажите:",
"keys.h.cline2": "Настройки → API Provider → OpenAI Compatible, затем вставьте значения выше.",
"keys.h.note": "cortiq-auto — умный роутинг: роутер сам выбирает модель под каждый запрос. Чтобы закрепить конкретную, укажите её точное имя. На Windows запускайте скрипты в Git Bash / WSL или задайте те же переменные через настройки окружения системы.",
"keys.h.protoOff": "Протокол Anthropic (/v1/messages) выключен — Claude Code не подключится.",
"keys.h.enableProto": "Включить",
"keys.h.st.installed": "Установлен на машине шлюза",
"keys.h.st.configured": "Уже настроен на этот шлюз",
"keys.h.st.notFound": "Не найден на машине шлюза",
};
+14
View File
@@ -602,4 +602,18 @@ export default {
"bm.spent": "harcanan",
"bm.verdict": "🏁 Bitti: yanıtlar {p}, harcanan {s}",
"bm.clearHistory": "Tümünü temizle",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — terminale yapıştırın (ağ geçidi Anthropic protokolünü doğal olarak konuşur):",
"keys.h.codex": "Codex — sağlayıcıyı ~/.codex/config.toml dosyasına ekler ve başlatır:",
"keys.h.opencode": "OpenCode — ~/.config/opencode/opencode.json dosyasını yazar (üzerine yazar!) ve başlatır:",
"keys.h.continue": "Continue (VS Code / JetBrains) — modeli ~/.continue/config.yaml dosyasına ekler:",
"keys.h.cline1": "Cline (VS Code) — eklenti ayarlarında şunları seçin:",
"keys.h.cline2": "Ayarlar → API Provider → OpenAI Compatible, ardından yukarıdaki değerleri yapıştırın.",
"keys.h.note": "cortiq-auto = akıllı yönlendirme: yönlendirici her istek için modeli kendisi seçer. Belirli bir model için tam adını yazın. Windows'ta betikleri Git Bash / WSL içinde çalıştırın veya aynı değişkenleri sistem ortamından ayarlayın.",
"keys.h.protoOff": "Anthropic protokolü (/v1/messages) kapalı — Claude Code bağlanamaz.",
"keys.h.enableProto": "Etkinleştir",
"keys.h.st.installed": "Ağ geçidi makinesinde kurulu",
"keys.h.st.configured": "Bu ağ geçidi için zaten yapılandırılmış",
"keys.h.st.notFound": "Ağ geçidi makinesinde bulunamadı",
};
+14
View File
@@ -602,4 +602,18 @@ export default {
"bm.spent": "花费",
"bm.verdict": "🏁 完成:回答 {p},花费 {s}",
"bm.clearHistory": "全部清除",
// harness auto-setup (API keys view)
"keys.h.claude": "Claude Code — 粘贴到终端(网关原生支持 Anthropic 协议):",
"keys.h.codex": "Codex — 将提供商追加到 ~/.codex/config.toml 并启动:",
"keys.h.opencode": "OpenCode — 写入 ~/.config/opencode/opencode.json(会覆盖!)并启动:",
"keys.h.continue": "Continue(VS Code / JetBrains)— 将模型追加到 ~/.continue/config.yaml:",
"keys.h.cline1": "Cline(VS Code)— 在扩展设置中选择:",
"keys.h.cline2": "设置 → API Provider → OpenAI Compatible,然后粘贴上面的值。",
"keys.h.note": "cortiq-auto = 智能路由:路由器为每个请求选择模型。要固定某个模型,请填写其精确名称。Windows 上请在 Git Bash / WSL 中运行脚本,或通过系统环境变量设置相同的值。",
"keys.h.protoOff": "Anthropic 协议(/v1/messages)已关闭 — Claude Code 无法连接。",
"keys.h.enableProto": "启用",
"keys.h.st.installed": "已安装在网关所在机器上",
"keys.h.st.configured": "已配置为使用此网关",
"keys.h.st.notFound": "网关所在机器上未找到",
};
+109 -6
View File
@@ -58,9 +58,64 @@ export async function renderKeys() {
// Keys are stored hashed-masked in the list, so the key itself is a
// placeholder unless one was just created on this page.
function snippetsCard(openMode) {
const base = location.origin + "/v1";
const origin = location.origin;
const base = origin + "/v1";
const key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">");
// Harness auto-setup scripts first (the gateway speaks OpenAI at /v1/* and
// Anthropic at /v1/messages, so Claude Code connects natively), SDKs after.
const snips = {
"Claude Code": `# ${t("keys.h.claude")}
export ANTHROPIC_BASE_URL="${origin}"
export ANTHROPIC_AUTH_TOKEN="${key}"
export ANTHROPIC_MODEL="cortiq-auto"
claude`,
Codex: `# ${t("keys.h.codex")}
mkdir -p ~/.codex && cat >> ~/.codex/config.toml <<'EOF'
model = "cortiq-auto"
model_provider = "cortiq"
[model_providers.cortiq]
name = "Cortiq Gateway"
base_url = "${base}"
env_key = "CORTIQ_API_KEY"
wire_api = "chat"
EOF
export CORTIQ_API_KEY="${key}"
codex`,
OpenCode: `# ${t("keys.h.opencode")}
mkdir -p ~/.config/opencode && cat > ~/.config/opencode/opencode.json <<'EOF'
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"cortiq": {
"npm": "@ai-sdk/openai-compatible",
"name": "Cortiq Gateway",
"options": { "baseURL": "${base}", "apiKey": "${key}" },
"models": { "cortiq-auto": { "name": "Cortiq Auto (smart routing)" } }
}
}
}
EOF
opencode`,
Continue: `# ${t("keys.h.continue")}
mkdir -p ~/.continue && cat >> ~/.continue/config.yaml <<'EOF'
models:
- name: Cortiq Auto
provider: openai
model: cortiq-auto
apiBase: ${base}
apiKey: ${key}
EOF`,
Cline: `${t("keys.h.cline1")}
API Provider: OpenAI Compatible
Base URL: ${base}
API Key: ${key}
Model ID: cortiq-auto
${t("keys.h.cline2")}`,
curl: `curl ${base}/chat/completions \\
-H "Authorization: Bearer ${key}" \\
-H "Content-Type: application/json" \\
@@ -82,20 +137,65 @@ const resp = await client.chat.completions.create({
});
console.log(resp.choices[0].message.content);`,
};
let cur = "curl";
let cur = "Claude Code";
const pre = h("pre", { class: "conv-log snippet" }, snips[cur]);
const seg = h(
"div",
{ class: "seg" },
...Object.keys(snips).map((k, i) =>
h("button", { class: i === 0 ? "active" : "", onClick: (e) => {
h("button", { class: (i === 0 ? "active" : "") + (["curl", "python", "javascript"].includes(k) ? " sdk" : ""), "data-snip": k, onClick: (e) => {
cur = k;
pre.textContent = snips[k];
protoWarn.style.display = k === "Claude Code" && protoWarn.childNodes.length ? "" : "none";
paintStatus();
seg.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
e.target.classList.add("active");
e.currentTarget.classList.add("active");
} }, k)
)
);
// Claude Code needs the anthropic_messages protocol — offer a one-click
// enable when it is off, right where the user will hit the problem.
const protoWarn = h("div", { style: "display:none; margin-top:8px" });
api.getProtocols().then((prot) => {
if (prot.anthropic_messages) return;
const fixBtn = h("button", { class: "btn sm primary" }, t("keys.h.enableProto"));
fixBtn.addEventListener("click", async () => {
try {
await api.putProtocols({ ...prot, anthropic_messages: true });
protoWarn.style.display = "none";
toast(t("common.saved"), "ok");
} catch (e) {
toast(String(e.message || e), "err");
}
});
mount(protoWarn, h("div", { class: "callout warn" }, h("span", {}, "⚠ " + t("keys.h.protoOff")), " ", fixBtn));
if (cur === "Claude Code") protoWarn.style.display = "";
}).catch(() => {});
// Detected harnesses: ✓ on the tab when installed on the gateway machine,
// status line under the snippet ("installed" / "already configured").
let detected = {};
const statusHost = h("div", { class: "small", style: "margin-top:6px" });
function paintStatus() {
const d = detected[cur];
if (!d) {
statusHost.textContent = "";
return;
}
statusHost.innerHTML = "";
if (d.configured) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.configured")));
else if (d.installed) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.installed")));
else mount(statusHost, h("span", { class: "muted" }, "○ " + t("keys.h.st.notFound")));
}
api.harnesses().then((d) => {
detected = d || {};
seg.querySelectorAll("button").forEach((b) => {
const st = detected[b.dataset.snip];
if (st && st.installed) b.append(" ✓");
});
paintStatus();
}).catch(() => {});
const copy = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy"));
copy.addEventListener("click", async () => {
await navigator.clipboard.writeText(pre.textContent);
@@ -107,8 +207,11 @@ console.log(resp.choices[0].message.content);`,
h("div", { class: "card-title" }, "🔌 " + t("keys.snip.title")),
h("div", { class: "muted small", style: "margin-bottom:8px" },
lastCreatedKey ? t("keys.snip.withKey") : openMode ? t("keys.snip.openMode") : t("keys.snip.hint")),
h("div", { class: "flex", style: "gap:8px; margin-bottom:8px" }, seg, h("div", { class: "grow" }), copy),
pre
h("div", { class: "flex", style: "gap:8px; margin-bottom:8px; flex-wrap:wrap" }, seg, h("div", { class: "grow" }), copy),
pre,
statusHost,
protoWarn,
h("div", { class: "muted small", style: "margin-top:6px" }, t("keys.h.note"))
);
}