feat(keys): harness snippets for every OS + install command when missing
The harness auto-setup scripts were bash-only (heredocs, mkdir -p) — useless on Windows. Every scripty snippet now exists in two flavors behind a 🐧🍎/🪟 switch (auto-detected from the browser): bash heredocs for Linux/macOS and PowerShell here-strings ($env:, New-Item, @'..'@, curl.exe with backtick continuations) for Windows. SDK snippets and GUI instructions stay OS-independent. And when detection says a harness is NOT installed on the gateway machine, the status line now offers its one-line install command for the selected OS (claude.ai install.sh / install.ps1, npm -g for Codex and OpenCode-on- Windows, code --install-extension for Continue/Cline) with a copy button. 1 string ×7 languages.
This commit is contained in:
Vendored
+1
@@ -643,4 +643,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "Überschreiten die Monatsausgaben des Kontos die Grenze, werden Anfragen mit 429 insufficient_quota abgelehnt.",
|
"keys.form.budgetHint": "Überschreiten die Monatsausgaben des Kontos die Grenze, werden Anfragen mit 429 insufficient_quota abgelehnt.",
|
||||||
"keys.budgetOf": "Budget {b}/Mon.",
|
"keys.budgetOf": "Budget {b}/Mon.",
|
||||||
"dash.kpi.orBalance": "OpenRouter-Guthaben",
|
"dash.kpi.orBalance": "OpenRouter-Guthaben",
|
||||||
|
"keys.h.install": "Installieren:",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -636,4 +636,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "Requests are rejected with 429 insufficient_quota once the account's month-to-date spend crosses this.",
|
"keys.form.budgetHint": "Requests are rejected with 429 insufficient_quota once the account's month-to-date spend crosses this.",
|
||||||
"keys.budgetOf": "budget {b}/mo",
|
"keys.budgetOf": "budget {b}/mo",
|
||||||
"dash.kpi.orBalance": "OpenRouter balance",
|
"dash.kpi.orBalance": "OpenRouter balance",
|
||||||
|
"keys.h.install": "Install:",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -643,4 +643,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "Cuando el gasto del mes supera el techo, las peticiones se rechazan con 429 insufficient_quota.",
|
"keys.form.budgetHint": "Cuando el gasto del mes supera el techo, las peticiones se rechazan con 429 insufficient_quota.",
|
||||||
"keys.budgetOf": "presupuesto {b}/mes",
|
"keys.budgetOf": "presupuesto {b}/mes",
|
||||||
"dash.kpi.orBalance": "Saldo OpenRouter",
|
"dash.kpi.orBalance": "Saldo OpenRouter",
|
||||||
|
"keys.h.install": "Instalar:",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -643,4 +643,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "Quand les dépenses du mois dépassent le plafond, les requêtes sont rejetées avec 429 insufficient_quota.",
|
"keys.form.budgetHint": "Quand les dépenses du mois dépassent le plafond, les requêtes sont rejetées avec 429 insufficient_quota.",
|
||||||
"keys.budgetOf": "budget {b}/mois",
|
"keys.budgetOf": "budget {b}/mois",
|
||||||
"dash.kpi.orBalance": "Solde OpenRouter",
|
"dash.kpi.orBalance": "Solde OpenRouter",
|
||||||
|
"keys.h.install": "Installer :",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -635,4 +635,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "Когда траты аккаунта за месяц превысят потолок, запросы отклоняются с 429 insufficient_quota.",
|
"keys.form.budgetHint": "Когда траты аккаунта за месяц превысят потолок, запросы отклоняются с 429 insufficient_quota.",
|
||||||
"keys.budgetOf": "бюджет {b}/мес",
|
"keys.budgetOf": "бюджет {b}/мес",
|
||||||
"dash.kpi.orBalance": "Баланс OpenRouter",
|
"dash.kpi.orBalance": "Баланс OpenRouter",
|
||||||
|
"keys.h.install": "Установить:",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -643,4 +643,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "Hesabın aylık harcaması tavanı aşınca istekler 429 insufficient_quota ile reddedilir.",
|
"keys.form.budgetHint": "Hesabın aylık harcaması tavanı aşınca istekler 429 insufficient_quota ile reddedilir.",
|
||||||
"keys.budgetOf": "bütçe {b}/ay",
|
"keys.budgetOf": "bütçe {b}/ay",
|
||||||
"dash.kpi.orBalance": "OpenRouter bakiyesi",
|
"dash.kpi.orBalance": "OpenRouter bakiyesi",
|
||||||
|
"keys.h.install": "Kurulum:",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
@@ -643,4 +643,5 @@ export default {
|
|||||||
"keys.form.budgetHint": "当账户当月支出超过上限时,请求将被拒绝并返回 429 insufficient_quota。",
|
"keys.form.budgetHint": "当账户当月支出超过上限时,请求将被拒绝并返回 429 insufficient_quota。",
|
||||||
"keys.budgetOf": "预算 {b}/月",
|
"keys.budgetOf": "预算 {b}/月",
|
||||||
"dash.kpi.orBalance": "OpenRouter 余额",
|
"dash.kpi.orBalance": "OpenRouter 余额",
|
||||||
|
"keys.h.install": "安装:",
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+109
-4
@@ -55,9 +55,73 @@ export async function renderKeys() {
|
|||||||
const origin = location.origin;
|
const origin = location.origin;
|
||||||
const base = origin + "/v1";
|
const base = origin + "/v1";
|
||||||
const key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">");
|
const key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">");
|
||||||
|
// Every scripty snippet exists in two OS flavors: bash heredocs for
|
||||||
|
// Linux/macOS and PowerShell here-strings for Windows. SDK snippets
|
||||||
|
// (python/js) and GUI instructions are OS-independent.
|
||||||
|
let os = /win/i.test(navigator.platform || "") ? "ps" : "sh";
|
||||||
// Harness auto-setup scripts first (the gateway speaks OpenAI at /v1/* and
|
// Harness auto-setup scripts first (the gateway speaks OpenAI at /v1/* and
|
||||||
// Anthropic at /v1/messages, so Claude Code connects natively), SDKs after.
|
// Anthropic at /v1/messages, so Claude Code connects natively), SDKs after.
|
||||||
const snips = {
|
const buildSnips = (osKind) => osKind === "ps" ? {
|
||||||
|
"Claude Code": `# ${t("keys.h.claude")}
|
||||||
|
$env:ANTHROPIC_BASE_URL = "${origin}"
|
||||||
|
$env:ANTHROPIC_AUTH_TOKEN = "${key}"
|
||||||
|
$env:ANTHROPIC_MODEL = "cortiq-auto"
|
||||||
|
|
||||||
|
claude`,
|
||||||
|
Codex: `# ${t("keys.h.codex")}
|
||||||
|
New-Item -ItemType Directory -Force "$HOME\\.codex" | Out-Null
|
||||||
|
Add-Content "$HOME\\.codex\\config.toml" @'
|
||||||
|
model = "cortiq-auto"
|
||||||
|
model_provider = "cortiq"
|
||||||
|
|
||||||
|
[model_providers.cortiq]
|
||||||
|
name = "Cortiq Gateway"
|
||||||
|
base_url = "${base}"
|
||||||
|
env_key = "CORTIQ_API_KEY"
|
||||||
|
wire_api = "chat"
|
||||||
|
'@
|
||||||
|
$env:CORTIQ_API_KEY = "${key}"
|
||||||
|
|
||||||
|
codex`,
|
||||||
|
OpenCode: `# ${t("keys.h.opencode")}
|
||||||
|
New-Item -ItemType Directory -Force "$HOME\\.config\\opencode" | Out-Null
|
||||||
|
Set-Content "$HOME\\.config\\opencode\\opencode.json" @'
|
||||||
|
{
|
||||||
|
"$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)" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
|
||||||
|
opencode`,
|
||||||
|
Continue: `# ${t("keys.h.continue")}
|
||||||
|
New-Item -ItemType Directory -Force "$HOME\\.continue" | Out-Null
|
||||||
|
Add-Content "$HOME\\.continue\\config.yaml" @'
|
||||||
|
models:
|
||||||
|
- name: Cortiq Auto
|
||||||
|
provider: openai
|
||||||
|
model: cortiq-auto
|
||||||
|
apiBase: ${base}
|
||||||
|
apiKey: ${key}
|
||||||
|
'@`,
|
||||||
|
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.exe ${base}/chat/completions \`
|
||||||
|
-H "Authorization: Bearer ${key}" \`
|
||||||
|
-H "Content-Type: application/json" \`
|
||||||
|
-d '{"model":"cortiq-auto","messages":[{"role":"user","content":"Привет!"}]}'`,
|
||||||
|
} : {
|
||||||
"Claude Code": `# ${t("keys.h.claude")}
|
"Claude Code": `# ${t("keys.h.claude")}
|
||||||
export ANTHROPIC_BASE_URL="${origin}"
|
export ANTHROPIC_BASE_URL="${origin}"
|
||||||
export ANTHROPIC_AUTH_TOKEN="${key}"
|
export ANTHROPIC_AUTH_TOKEN="${key}"
|
||||||
@@ -131,6 +195,11 @@ const resp = await client.chat.completions.create({
|
|||||||
});
|
});
|
||||||
console.log(resp.choices[0].message.content);`,
|
console.log(resp.choices[0].message.content);`,
|
||||||
};
|
};
|
||||||
|
const shSnips = buildSnips("sh");
|
||||||
|
const psSnips = buildSnips("ps");
|
||||||
|
// python/js/Cline одинаковы — дополняем ps-набор из sh
|
||||||
|
for (const k of Object.keys(shSnips)) if (!psSnips[k]) psSnips[k] = shSnips[k];
|
||||||
|
let snips = os === "ps" ? psSnips : shSnips;
|
||||||
let cur = "Claude Code";
|
let cur = "Claude Code";
|
||||||
const pre = h("pre", { class: "conv-log snippet" }, snips[cur]);
|
const pre = h("pre", { class: "conv-log snippet" }, snips[cur]);
|
||||||
const seg = h(
|
const seg = h(
|
||||||
@@ -167,7 +236,15 @@ console.log(resp.choices[0].message.content);`,
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
// Detected harnesses: ✓ on the tab when installed on the gateway machine,
|
// Detected harnesses: ✓ on the tab when installed on the gateway machine,
|
||||||
// status line under the snippet ("installed" / "already configured").
|
// status line under the snippet ("installed" / "already configured") —
|
||||||
|
// and a one-line INSTALL command (per OS) when it's missing.
|
||||||
|
const INSTALL = {
|
||||||
|
"Claude Code": { sh: "curl -fsSL https://claude.ai/install.sh | bash", ps: "irm https://claude.ai/install.ps1 | iex" },
|
||||||
|
Codex: { sh: "npm install -g @openai/codex", ps: "npm install -g @openai/codex" },
|
||||||
|
OpenCode: { sh: "curl -fsSL https://opencode.ai/install | bash", ps: "npm install -g opencode-ai" },
|
||||||
|
Continue: { sh: "code --install-extension Continue.continue", ps: "code --install-extension Continue.continue" },
|
||||||
|
Cline: { sh: "code --install-extension saoudrizwan.claude-dev", ps: "code --install-extension saoudrizwan.claude-dev" },
|
||||||
|
};
|
||||||
let detected = {};
|
let detected = {};
|
||||||
const statusHost = h("div", { class: "small", style: "margin-top:6px" });
|
const statusHost = h("div", { class: "small", style: "margin-top:6px" });
|
||||||
function paintStatus() {
|
function paintStatus() {
|
||||||
@@ -179,7 +256,19 @@ console.log(resp.choices[0].message.content);`,
|
|||||||
statusHost.innerHTML = "";
|
statusHost.innerHTML = "";
|
||||||
if (d.configured) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.configured")));
|
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 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")));
|
else {
|
||||||
|
const cmd = (INSTALL[cur] || {})[os];
|
||||||
|
const copyInstall = h("button", { class: "btn ghost sm", onClick: async () => {
|
||||||
|
await navigator.clipboard.writeText(cmd);
|
||||||
|
toast(t("common.copied"), "good");
|
||||||
|
} }, "⧉");
|
||||||
|
mount(statusHost,
|
||||||
|
h("span", { class: "muted" }, "○ " + t("keys.h.st.notFound")),
|
||||||
|
cmd ? h("div", { class: "flex", style: "gap:6px; align-items:center; margin-top:4px" },
|
||||||
|
h("span", { class: "muted" }, t("keys.h.install")),
|
||||||
|
h("code", { class: "mono small", style: "background:var(--panel-2,#23252c); padding:2px 8px; border-radius:6px" }, cmd),
|
||||||
|
copyInstall) : null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
api.harnesses().then((d) => {
|
api.harnesses().then((d) => {
|
||||||
detected = d || {};
|
detected = d || {};
|
||||||
@@ -190,6 +279,22 @@ console.log(resp.choices[0].message.content);`,
|
|||||||
paintStatus();
|
paintStatus();
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
|
// OS switch: bash for Linux/macOS, PowerShell for Windows (auto-detected)
|
||||||
|
const osSeg = h(
|
||||||
|
"div",
|
||||||
|
{ class: "seg" },
|
||||||
|
h("button", { class: os === "sh" ? "active" : "" }, "🐧🍎 Linux / macOS"),
|
||||||
|
h("button", { class: os === "ps" ? "active" : "" }, "🪟 Windows")
|
||||||
|
);
|
||||||
|
osSeg.querySelectorAll("button").forEach((b, i) =>
|
||||||
|
b.addEventListener("click", () => {
|
||||||
|
os = i === 0 ? "sh" : "ps";
|
||||||
|
snips = os === "ps" ? psSnips : shSnips;
|
||||||
|
osSeg.querySelectorAll("button").forEach((x, j) => x.classList.toggle("active", j === (os === "sh" ? 0 : 1)));
|
||||||
|
pre.textContent = snips[cur];
|
||||||
|
paintStatus();
|
||||||
|
}));
|
||||||
|
|
||||||
const copy = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy"));
|
const copy = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy"));
|
||||||
copy.addEventListener("click", async () => {
|
copy.addEventListener("click", async () => {
|
||||||
await navigator.clipboard.writeText(pre.textContent);
|
await navigator.clipboard.writeText(pre.textContent);
|
||||||
@@ -201,7 +306,7 @@ console.log(resp.choices[0].message.content);`,
|
|||||||
h("div", { class: "card-title" }, "🔌 " + t("keys.snip.title")),
|
h("div", { class: "card-title" }, "🔌 " + t("keys.snip.title")),
|
||||||
h("div", { class: "muted small", style: "margin-bottom:8px" },
|
h("div", { class: "muted small", style: "margin-bottom:8px" },
|
||||||
lastCreatedKey ? t("keys.snip.withKey") : openMode ? t("keys.snip.openMode") : t("keys.snip.hint")),
|
lastCreatedKey ? t("keys.snip.withKey") : openMode ? t("keys.snip.openMode") : t("keys.snip.hint")),
|
||||||
h("div", { class: "flex", style: "gap:8px; margin-bottom:8px; flex-wrap:wrap" }, seg, h("div", { class: "grow" }), copy),
|
h("div", { class: "flex", style: "gap:8px; margin-bottom:8px; flex-wrap:wrap" }, seg, h("div", { class: "grow" }), osSeg, copy),
|
||||||
pre,
|
pre,
|
||||||
statusHost,
|
statusHost,
|
||||||
protoWarn,
|
protoWarn,
|
||||||
|
|||||||
Reference in New Issue
Block a user