@s-berdia/opencode-router 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/USAGE.md +75 -6
- package/dist/bin.js +161 -12
- package/dist/bin.js.map +1 -1
- package/dist/index.js +92 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -923,10 +923,13 @@ function globalConfigDir() {
|
|
|
923
923
|
const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
|
|
924
924
|
return path.join(base, "opencode");
|
|
925
925
|
}
|
|
926
|
-
function
|
|
926
|
+
function opencodeCacheDir() {
|
|
927
927
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
928
928
|
const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".cache");
|
|
929
|
-
return path.join(base, "opencode"
|
|
929
|
+
return path.join(base, "opencode");
|
|
930
|
+
}
|
|
931
|
+
function catalogCacheFile() {
|
|
932
|
+
return path.join(opencodeCacheDir(), "models.json");
|
|
930
933
|
}
|
|
931
934
|
function globalRouterConfigFile() {
|
|
932
935
|
return path.join(globalConfigDir(), "router.config.json");
|
|
@@ -961,6 +964,10 @@ function readJsonFile(file) {
|
|
|
961
964
|
if (parsed.error !== void 0) return { status: "invalid", error: parsed.error };
|
|
962
965
|
return { status: "ok", value: parsed.value };
|
|
963
966
|
}
|
|
967
|
+
function readJson(file) {
|
|
968
|
+
const result = readJsonFile(file);
|
|
969
|
+
return result.status === "ok" ? result.value : void 0;
|
|
970
|
+
}
|
|
964
971
|
function isPlainObject(value) {
|
|
965
972
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
966
973
|
}
|
|
@@ -1222,6 +1229,7 @@ function sanitizeUserConfig(raw, file, notes) {
|
|
|
1222
1229
|
if (threshold !== void 0 && !(typeof threshold === "number" && threshold > 0 && threshold <= 1)) {
|
|
1223
1230
|
drop("softThreshold", "must be a number above 0 and at most 1");
|
|
1224
1231
|
}
|
|
1232
|
+
if (config.updateCheck !== void 0 && typeof config.updateCheck !== "boolean") drop("updateCheck", "must be true or false");
|
|
1225
1233
|
for (const key of ["denyModels", "allowModels"]) {
|
|
1226
1234
|
const list = config[key];
|
|
1227
1235
|
if (list !== void 0 && !(Array.isArray(list) && list.every((item) => typeof item === "string"))) {
|
|
@@ -1816,8 +1824,13 @@ var SLASH_COMMANDS = [
|
|
|
1816
1824
|
{ name: "refresh-routing", description: "Recompute and apply OpenCode model routing", args: "refresh" },
|
|
1817
1825
|
{ name: "routing-status", description: "Show the model in use per role and its 5-hour quota use", args: "status" },
|
|
1818
1826
|
{ name: "routing-plan", description: "Switch the routing plan (go | go-plus | free) and rebuild", args: "plan $1" },
|
|
1819
|
-
{ name: "routing-eval", description: "Run the router scoring stress test", args: "eval" }
|
|
1827
|
+
{ name: "routing-eval", description: "Run the router scoring stress test", args: "eval" },
|
|
1828
|
+
{ name: "routing-update", description: "Update opencode-router to the newest version", args: "update" }
|
|
1820
1829
|
];
|
|
1830
|
+
var OUTPUT_BEGIN = "---BEGIN ROUTER OUTPUT---";
|
|
1831
|
+
function isRouterCommandPrompt(parts) {
|
|
1832
|
+
return parts.some((part) => part.type === "text" && part.text?.includes(OUTPUT_BEGIN) === true);
|
|
1833
|
+
}
|
|
1821
1834
|
function cliScriptPath() {
|
|
1822
1835
|
return fileURLToPath(new URL("./bin.js", import.meta.url)).replace(/\\/g, "/");
|
|
1823
1836
|
}
|
|
@@ -1830,7 +1843,7 @@ function commandTemplate(args) {
|
|
|
1830
1843
|
"2. Present the text between the markers to the user exactly as written.",
|
|
1831
1844
|
"3. Do not summarize, reformat, shorten, or reorganize it.",
|
|
1832
1845
|
"",
|
|
1833
|
-
|
|
1846
|
+
OUTPUT_BEGIN,
|
|
1834
1847
|
`!\`node "${cliScriptPath()}" ${args} --scope global\``,
|
|
1835
1848
|
"---END ROUTER OUTPUT---"
|
|
1836
1849
|
].join("\n");
|
|
@@ -1848,6 +1861,9 @@ function ledgerFile() {
|
|
|
1848
1861
|
function routingTableFile() {
|
|
1849
1862
|
return path3.join(stateDir(), "routing-table.json");
|
|
1850
1863
|
}
|
|
1864
|
+
function updateCheckFile() {
|
|
1865
|
+
return path3.join(stateDir(), "update-check.json");
|
|
1866
|
+
}
|
|
1851
1867
|
function applyMarkerFile() {
|
|
1852
1868
|
return path3.join(stateDir(), "last-apply.json");
|
|
1853
1869
|
}
|
|
@@ -1858,6 +1874,66 @@ function clearApplyMarker() {
|
|
|
1858
1874
|
fs4.rmSync(applyMarkerFile(), { force: true });
|
|
1859
1875
|
}
|
|
1860
1876
|
|
|
1877
|
+
// src/update.ts
|
|
1878
|
+
import fs5 from "fs";
|
|
1879
|
+
import path4 from "path";
|
|
1880
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1881
|
+
var PACKAGE_NAME = "@s-berdia/opencode-router";
|
|
1882
|
+
var LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2f")}/latest`;
|
|
1883
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1884
|
+
var TIMEOUT_MS = 3e3;
|
|
1885
|
+
function parseVersion(version) {
|
|
1886
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
1887
|
+
return match ? match.slice(1).map(Number) : null;
|
|
1888
|
+
}
|
|
1889
|
+
function isNewerVersion(a, b) {
|
|
1890
|
+
const left = parseVersion(a);
|
|
1891
|
+
const right = parseVersion(b);
|
|
1892
|
+
if (!left || !right) return false;
|
|
1893
|
+
for (let i = 0; i < 3; i++) {
|
|
1894
|
+
if (left[i] !== right[i]) return left[i] > right[i];
|
|
1895
|
+
}
|
|
1896
|
+
return false;
|
|
1897
|
+
}
|
|
1898
|
+
function installedVersion() {
|
|
1899
|
+
const file = fileURLToPath2(new URL("../package.json", import.meta.url));
|
|
1900
|
+
return readJson(file)?.version ?? "0.0.0";
|
|
1901
|
+
}
|
|
1902
|
+
async function fetchLatestVersion() {
|
|
1903
|
+
const controller = new AbortController();
|
|
1904
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1905
|
+
try {
|
|
1906
|
+
const res = await fetch(LATEST_URL, { signal: controller.signal, headers: { accept: "application/json" } });
|
|
1907
|
+
if (!res.ok) return void 0;
|
|
1908
|
+
const body = await res.json();
|
|
1909
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
1910
|
+
} catch {
|
|
1911
|
+
return void 0;
|
|
1912
|
+
} finally {
|
|
1913
|
+
clearTimeout(timeout);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
async function checkForUpdate(opts = {}) {
|
|
1917
|
+
try {
|
|
1918
|
+
if (loadUserConfig(opts.projectDir).config.updateCheck === false) return null;
|
|
1919
|
+
const current = opts.current ?? installedVersion();
|
|
1920
|
+
const now = opts.now ?? Date.now();
|
|
1921
|
+
const state = readJson(updateCheckFile());
|
|
1922
|
+
let latest = state?.latest;
|
|
1923
|
+
const fresh = state !== void 0 && now >= state.checkedAt && now - state.checkedAt < CHECK_INTERVAL_MS;
|
|
1924
|
+
if (!fresh) {
|
|
1925
|
+
latest = await fetchLatestVersion() ?? latest;
|
|
1926
|
+
writeJson(updateCheckFile(), { checkedAt: now, latest });
|
|
1927
|
+
}
|
|
1928
|
+
return latest && isNewerVersion(latest, current) ? { current, latest } : null;
|
|
1929
|
+
} catch {
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function updateNotice(update) {
|
|
1934
|
+
return `opencode-router ${update.latest} is available (installed: ${update.current}). To update, type /routing-update in OpenCode.`;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1861
1937
|
// src/index.ts
|
|
1862
1938
|
var RATE_LIMIT_COOLDOWN_MS = 60 * 60 * 1e3;
|
|
1863
1939
|
var AUTH_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1883,6 +1959,8 @@ var OpencodeRouter = async (input) => {
|
|
|
1883
1959
|
const ledger = new LedgerStore(ledgerFile(), () => Date.now());
|
|
1884
1960
|
const recordedMessages = /* @__PURE__ */ new Set();
|
|
1885
1961
|
const sessionModels = /* @__PURE__ */ new Map();
|
|
1962
|
+
const pendingUpdate = checkForUpdate({ projectDir: dir });
|
|
1963
|
+
let updateToastShown = false;
|
|
1886
1964
|
let cached = null;
|
|
1887
1965
|
let cachedAt = 0;
|
|
1888
1966
|
let blendedById = /* @__PURE__ */ new Map();
|
|
@@ -1959,6 +2037,7 @@ var OpencodeRouter = async (input) => {
|
|
|
1959
2037
|
try {
|
|
1960
2038
|
const agent = msgInput.agent ?? output.message.agent;
|
|
1961
2039
|
if (!isAgentRole(agent)) return;
|
|
2040
|
+
if (isRouterCommandPrompt(output.parts)) return;
|
|
1962
2041
|
const result = await ensure();
|
|
1963
2042
|
const entry = pickForRole(result.table, agent, healthCheck(result, ledger));
|
|
1964
2043
|
if (!entry) return;
|
|
@@ -1987,6 +2066,15 @@ var OpencodeRouter = async (input) => {
|
|
|
1987
2066
|
ledger.record(id, cost, tokens);
|
|
1988
2067
|
if (isRateLimitError(info.error)) ledger.cooldown(id, RATE_LIMIT_COOLDOWN_MS, "rate limit / quota");
|
|
1989
2068
|
else if (isAuthError(info.error)) ledger.cooldown(id, AUTH_COOLDOWN_MS, "auth error");
|
|
2069
|
+
} else if (event.type === "session.created" && !updateToastShown) {
|
|
2070
|
+
updateToastShown = true;
|
|
2071
|
+
void pendingUpdate.then((update) => {
|
|
2072
|
+
if (!update) return;
|
|
2073
|
+
return input.client.tui.showToast({
|
|
2074
|
+
body: { title: "opencode-router", message: updateNotice(update), variant: "info", duration: 1e4 }
|
|
2075
|
+
});
|
|
2076
|
+
}).catch(() => {
|
|
2077
|
+
});
|
|
1990
2078
|
} else if (event.type === "session.error") {
|
|
1991
2079
|
const { sessionID, error } = event.properties;
|
|
1992
2080
|
const id = sessionID ? sessionModels.get(sessionID) : void 0;
|