isorite 0.5.0 → 0.6.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 +1 -1
- package/dist/main.js +460 -99
- package/docs/setup-for-agents.md +5 -1
- package/docs/setup.md +18 -4
- package/package.json +1 -1
- package/web/_app/immutable/assets/{0.DNvf5eFW.css → 0.LEIHnkPh.css} +1 -1
- package/web/_app/immutable/chunks/{DG2llOXe.js → 2DHY0Dzh.js} +3 -3
- package/web/_app/immutable/chunks/{C1xA2mdF.js → BQRMn_tl.js} +1 -1
- package/web/_app/immutable/chunks/C-352d8F.js +1 -0
- package/web/_app/immutable/chunks/{B9vLu37F.js → CHxNLteG.js} +1 -1
- package/web/_app/immutable/chunks/Cf6h5DpS.js +20 -0
- package/web/_app/immutable/chunks/D4A7-5k0.js +1 -0
- package/web/_app/immutable/chunks/Dijhflxl.js +1 -0
- package/web/_app/immutable/chunks/Ps0IPbdL.js +1 -0
- package/web/_app/immutable/chunks/{D4Jg9Smi.js → ZRnI3PpK.js} +1 -1
- package/web/_app/immutable/chunks/nhmVyQdD.js +3 -0
- package/web/_app/immutable/chunks/rTqzqD6j.js +1 -0
- package/web/_app/immutable/chunks/y24aTX4O.js +1 -0
- package/web/_app/immutable/entry/app.BQ0r6jSu.js +2 -0
- package/web/_app/immutable/entry/start.CIQiYrSU.js +1 -0
- package/web/_app/immutable/nodes/0.zPrOkn79.js +26 -0
- package/web/_app/immutable/nodes/1.DfBkOAo2.js +1 -0
- package/web/_app/immutable/nodes/2.fp8Y5joG.js +1 -0
- package/web/_app/immutable/nodes/3.Do9cwxjS.js +1 -0
- package/web/_app/immutable/nodes/4.CGd6gzGJ.js +2 -0
- package/web/_app/immutable/nodes/5.Dtac-HK8.js +5 -0
- package/web/_app/immutable/nodes/6._-kZkWYe.js +1 -0
- package/web/_app/immutable/nodes/7.BdTE6U4H.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +17 -17
- package/web/_app/immutable/chunks/-7PsQOY4.js +0 -3
- package/web/_app/immutable/chunks/BAVeNK5c.js +0 -20
- package/web/_app/immutable/chunks/CQqNkDRK.js +0 -1
- package/web/_app/immutable/chunks/Cs9sQ8Oa.js +0 -1
- package/web/_app/immutable/chunks/DqVFJWdL.js +0 -1
- package/web/_app/immutable/chunks/HCrXwzvD.js +0 -1
- package/web/_app/immutable/chunks/MjV3DsQW.js +0 -1
- package/web/_app/immutable/chunks/uID4v_1K.js +0 -1
- package/web/_app/immutable/entry/app.CR9ubCCo.js +0 -2
- package/web/_app/immutable/entry/start.D4DOZCP6.js +0 -1
- package/web/_app/immutable/nodes/0.BYXv43VP.js +0 -26
- package/web/_app/immutable/nodes/1.DGP71l35.js +0 -1
- package/web/_app/immutable/nodes/2.CxspTYRG.js +0 -1
- package/web/_app/immutable/nodes/3.ZTzs6oEI.js +0 -1
- package/web/_app/immutable/nodes/4.CeIGu2c2.js +0 -2
- package/web/_app/immutable/nodes/5.k1DmYkGm.js +0 -5
- package/web/_app/immutable/nodes/6.Q0yGno8p.js +0 -1
- package/web/_app/immutable/nodes/7.BJRu312o.js +0 -1
package/dist/main.js
CHANGED
|
@@ -62,7 +62,7 @@ function cmpTuple(a, b) {
|
|
|
62
62
|
}
|
|
63
63
|
/** A frontmatter value as text: true/false, whole-number floats without ".0",
|
|
64
64
|
* nothing for null. */
|
|
65
|
-
function text(value) {
|
|
65
|
+
function text$1(value) {
|
|
66
66
|
if (value === null || value === void 0) return "";
|
|
67
67
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
68
68
|
return String(value);
|
|
@@ -450,14 +450,14 @@ function kindOf(type) {
|
|
|
450
450
|
}
|
|
451
451
|
/** Note ids from frontmatter: a list, or one; `/a/b.md` and `a/b` alike. */
|
|
452
452
|
function idsOf(raw) {
|
|
453
|
-
return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text(t).trim().replace(/^\/+/, "").replace(/\.md$/, "")).filter(Boolean);
|
|
453
|
+
return (typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []).map((t) => text$1(t).trim().replace(/^\/+/, "").replace(/\.md$/, "")).filter(Boolean);
|
|
454
454
|
}
|
|
455
455
|
/** When it was resolved or solved, as written (a date), or null while it is open. */
|
|
456
456
|
function doneOf(meta, kind) {
|
|
457
457
|
if (kind === "note") return null;
|
|
458
458
|
const v = meta[DONE[kind]];
|
|
459
459
|
if (v === null || v === void 0 || v === false || v === "") return null;
|
|
460
|
-
return v instanceof Date ? v.toISOString().slice(0, 10) : text(v);
|
|
460
|
+
return v instanceof Date ? v.toISOString().slice(0, 10) : text$1(v);
|
|
461
461
|
}
|
|
462
462
|
/** The embeds in a note's body, in order, as written (a name, or a path). */
|
|
463
463
|
function embedRefs(body) {
|
|
@@ -575,17 +575,17 @@ var init_bundle = __esmMin((() => {
|
|
|
575
575
|
}
|
|
576
576
|
get type() {
|
|
577
577
|
const t = this.meta.type;
|
|
578
|
-
return t === null || t === void 0 || t === "" ? "" : text(t);
|
|
578
|
+
return t === null || t === void 0 || t === "" ? "" : text$1(t);
|
|
579
579
|
}
|
|
580
580
|
get title() {
|
|
581
581
|
const t = this.meta.title;
|
|
582
|
-
if (t) return text(t);
|
|
582
|
+
if (t) return text$1(t);
|
|
583
583
|
const stem = basename$1(this.id);
|
|
584
584
|
const words = strip(stem.replaceAll("-", " ").replaceAll("_", " "));
|
|
585
585
|
return words ? words[0].toUpperCase() + words.slice(1).toLowerCase() : stem;
|
|
586
586
|
}
|
|
587
587
|
get description() {
|
|
588
|
-
return text(this.meta.description || "");
|
|
588
|
+
return text$1(this.meta.description || "");
|
|
589
589
|
}
|
|
590
590
|
/** A note, an open question, or a problem (M12). */
|
|
591
591
|
get kind() {
|
|
@@ -598,10 +598,10 @@ var init_bundle = __esmMin((() => {
|
|
|
598
598
|
get tags() {
|
|
599
599
|
let tags = this.meta.tags || [];
|
|
600
600
|
if (typeof tags === "string") tags = [tags];
|
|
601
|
-
return Array.isArray(tags) ? tags.filter((t) => t !== null && t !== void 0).map(text) : [];
|
|
601
|
+
return Array.isArray(tags) ? tags.filter((t) => t !== null && t !== void 0).map(text$1) : [];
|
|
602
602
|
}
|
|
603
603
|
get status() {
|
|
604
|
-
return text(this.meta.status || "stable");
|
|
604
|
+
return text$1(this.meta.status || "stable");
|
|
605
605
|
}
|
|
606
606
|
get generated() {
|
|
607
607
|
return isEntry(this.meta.generated) ? this.meta.generated : {};
|
|
@@ -618,10 +618,10 @@ var init_bundle = __esmMin((() => {
|
|
|
618
618
|
get trust() {
|
|
619
619
|
const events = this.verified;
|
|
620
620
|
if (!events.length) return "unverified";
|
|
621
|
-
return events.some((e) => text(e.by).startsWith("human:")) ? "human-reviewed" : "machine-confirmed";
|
|
621
|
+
return events.some((e) => text$1(e.by).startsWith("human:")) ? "human-reviewed" : "machine-confirmed";
|
|
622
622
|
}
|
|
623
623
|
get lastHumanVerification() {
|
|
624
|
-
const times = this.verified.filter((e) => text(e.by).startsWith("human:")).map((e) => toTime(e.at)).filter((t) => t !== null);
|
|
624
|
+
const times = this.verified.filter((e) => text$1(e.by).startsWith("human:")).map((e) => toTime(e.at)).filter((t) => t !== null);
|
|
625
625
|
return times.length ? Math.max(...times) : null;
|
|
626
626
|
}
|
|
627
627
|
/** Human-reviewed, but meaningfully changed since the latest human check. */
|
|
@@ -1757,14 +1757,14 @@ function parseStudyConfig(source) {
|
|
|
1757
1757
|
}
|
|
1758
1758
|
const base = raw.base ?? {};
|
|
1759
1759
|
const format = raw.format === void 0 ? 1 : Number(raw.format);
|
|
1760
|
-
if (format !== 1) throw new StudyError(`${CONFIG_FILE} is format ${text(raw.format)}; this isorite reads format 1`);
|
|
1760
|
+
if (format !== 1) throw new StudyError(`${CONFIG_FILE} is format ${text$1(raw.format)}; this isorite reads format 1`);
|
|
1761
1761
|
if (typeof base.path !== "string" || !base.path) throw new StudyError(`${CONFIG_FILE} needs [base] path, the knowledge base's folder`);
|
|
1762
1762
|
return {
|
|
1763
1763
|
format,
|
|
1764
1764
|
base: {
|
|
1765
1765
|
path: base.path,
|
|
1766
|
-
remote: text(base.remote ?? ""),
|
|
1767
|
-
root: text(base.root ?? "")
|
|
1766
|
+
remote: text$1(base.remote ?? ""),
|
|
1767
|
+
root: text$1(base.root ?? "")
|
|
1768
1768
|
}
|
|
1769
1769
|
};
|
|
1770
1770
|
}
|
|
@@ -4365,6 +4365,14 @@ var init_engine = __esmMin((() => {
|
|
|
4365
4365
|
get provider() {
|
|
4366
4366
|
return this.opts.provider ?? OPENROUTER;
|
|
4367
4367
|
}
|
|
4368
|
+
/** Take another provider into use, from the next request on: a setup kept from the settings (M18). What was
|
|
4369
|
+
* asked of the last one (its key's limit, its model's window) is asked again. */
|
|
4370
|
+
setProvider(provider, http) {
|
|
4371
|
+
this.opts.provider = provider;
|
|
4372
|
+
this.opts.http = http;
|
|
4373
|
+
this.window = void 0;
|
|
4374
|
+
this.keyLimit = void 0;
|
|
4375
|
+
}
|
|
4368
4376
|
async settings() {
|
|
4369
4377
|
return {
|
|
4370
4378
|
provider: this.provider.name,
|
|
@@ -4561,7 +4569,7 @@ var init_engine = __esmMin((() => {
|
|
|
4561
4569
|
async begin(req, http, asked) {
|
|
4562
4570
|
if (asked.length > 4e3) throw new EngineError("that is too long to ask at once", 413);
|
|
4563
4571
|
const key = await this.opts.secrets?.get("provider.key") ?? null;
|
|
4564
|
-
if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in settings
|
|
4572
|
+
if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in Iso's settings.", 409);
|
|
4565
4573
|
const { messages, olds } = await this.compose(req, asked);
|
|
4566
4574
|
const maxTokens = req.form === "insert" ? REPLY_TOKENS : req.form === "note" ? NOTE_TOKENS : Math.max(REPLY_TOKENS, Math.ceil(olds.join("").length / 3) + 1e3);
|
|
4567
4575
|
const most = (req.form === "insert" || req.form === "rewrite") && req.choice?.lookup === false ? 0 : void 0;
|
|
@@ -5506,7 +5514,7 @@ var init_engine = __esmMin((() => {
|
|
|
5506
5514
|
async beginTurn(conversation, req, http) {
|
|
5507
5515
|
const study = this.opts.own;
|
|
5508
5516
|
const key = await this.opts.secrets?.get("provider.key") ?? null;
|
|
5509
|
-
if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in settings
|
|
5517
|
+
if (!key && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(this.provider.url)) throw new EngineError("There is no provider key yet. Set one in Iso's settings.", 409);
|
|
5510
5518
|
const lookupOn = req.lookup !== false, mode = req.mode;
|
|
5511
5519
|
let w = await this.working({
|
|
5512
5520
|
...req,
|
|
@@ -6702,7 +6710,11 @@ const textTable = (field, v, env) => v === void 0 ? void 0 : Object.fromEntries(
|
|
|
6702
6710
|
* `headers`, `key_header`, `query` and `stream_usage = false`; `max_tokens` for models that think at length; its
|
|
6703
6711
|
* client certificate is `gatewayTls`'s. */
|
|
6704
6712
|
function providerConfig(path = configPath(), env = process.env) {
|
|
6705
|
-
|
|
6713
|
+
return providerFrom(readConfig(path), env);
|
|
6714
|
+
}
|
|
6715
|
+
/** The provider a configuration names, as `providerConfig` reads it from the file: for a setup not yet kept (M18). */
|
|
6716
|
+
function providerFrom(config, env = process.env) {
|
|
6717
|
+
const p = table(config.provider), tiers = table(config.tiers), prices = table(config.prices);
|
|
6706
6718
|
const custom = typeof p.url === "string" && p.url !== "";
|
|
6707
6719
|
const base = custom ? {
|
|
6708
6720
|
name: typeof p.name === "string" && p.name ? p.name : "custom",
|
|
@@ -6733,10 +6745,14 @@ function providerConfig(path = configPath(), env = process.env) {
|
|
|
6733
6745
|
return out;
|
|
6734
6746
|
}
|
|
6735
6747
|
function gatewayTls(path = configPath(), env = process.env) {
|
|
6736
|
-
|
|
6748
|
+
return gatewayTlsFrom(readConfig(path), dirname(path), env);
|
|
6749
|
+
}
|
|
6750
|
+
/** The certificate a configuration names, its paths from `dir` (the configuration's folder). */
|
|
6751
|
+
function gatewayTlsFrom(config, dir, env = process.env) {
|
|
6752
|
+
const p = table(config.provider), files = [];
|
|
6737
6753
|
const file = (field) => {
|
|
6738
|
-
if (p[field] === void 0) return void 0;
|
|
6739
|
-
const named = valueOf(field, p[field], env), at = named.startsWith("~/") ? join(homedir(), named.slice(2)) : resolve(
|
|
6754
|
+
if (p[field] === void 0 || p[field] === "") return void 0;
|
|
6755
|
+
const named = valueOf(field, p[field], env), at = named.startsWith("~/") ? join(homedir(), named.slice(2)) : resolve(dir, named);
|
|
6740
6756
|
if (!existsSync(at)) throw new Error(`[provider] ${field}: there is no file at ${at}`);
|
|
6741
6757
|
files.push(at);
|
|
6742
6758
|
return readFileSync(at);
|
|
@@ -6754,6 +6770,10 @@ function gatewayTls(path = configPath(), env = process.env) {
|
|
|
6754
6770
|
if (p.pfx_passphrase !== void 0) tls.passphrase = valueOf("pfx_passphrase", p.pfx_passphrase, env);
|
|
6755
6771
|
return files.length ? tls : null;
|
|
6756
6772
|
}
|
|
6773
|
+
/** Whether Iso has a model here: a key kept, or a provider of your own (a gateway may need no key). */
|
|
6774
|
+
async function hasModel$1(config = configPath()) {
|
|
6775
|
+
return await new ConfigSecrets(config).get("provider.key") !== null || providerConfig(config).url !== OPENROUTER.url;
|
|
6776
|
+
}
|
|
6757
6777
|
//#endregion
|
|
6758
6778
|
//#region packages/server/src/model.ts
|
|
6759
6779
|
/** Where the server looks for the model, in order, and why each place is there. */
|
|
@@ -6784,7 +6804,7 @@ function modelPlaces(env = process.env) {
|
|
|
6784
6804
|
/** Where the model is, or null when it is not installed or is switched off (ISORITE_EMBED=off). */
|
|
6785
6805
|
function modelDir(env = process.env) {
|
|
6786
6806
|
if (env.ISORITE_EMBED === "off") return null;
|
|
6787
|
-
return modelPlaces(env).find((p) => hasModel
|
|
6807
|
+
return modelPlaces(env).find((p) => hasModel(p.dir))?.dir ?? null;
|
|
6788
6808
|
}
|
|
6789
6809
|
/** What is in a model's folder. `isorite-model.json` beside the files may give its `name` and `pooling` ("cls",
|
|
6790
6810
|
* the first token's state, as granite is used; or "mean", the average of them all, as all-MiniLM is). */
|
|
@@ -6918,7 +6938,7 @@ async function fetchModel(out, say = console.log, { from, pkg = PACKAGE, release
|
|
|
6918
6938
|
* checked by using it: it must load, and give DIM numbers for a sentence. Kept in the user's models, and used from
|
|
6919
6939
|
* then on. */
|
|
6920
6940
|
async function installFrom(from, say = console.log, env = process.env) {
|
|
6921
|
-
if (!hasModel
|
|
6941
|
+
if (!hasModel(from)) throw new Error(`${from} has no model: it needs model.onnx and tokenizer.json`);
|
|
6922
6942
|
const granite = !existsSync(join(from, "isorite-model.json")) && FILES.every(([, to, want]) => existsSync(join(from, to)) && sha(readFileSync(join(from, to))) === want);
|
|
6923
6943
|
let out = userModelDir(env);
|
|
6924
6944
|
if (granite) await fetchModel(out, say, { from });
|
|
@@ -6955,7 +6975,7 @@ async function modelCommand(from, say = console.log, env = process.env, sources
|
|
|
6955
6975
|
say(e.message);
|
|
6956
6976
|
return 1;
|
|
6957
6977
|
}
|
|
6958
|
-
const places = modelPlaces(env), found = places.find((p) => hasModel
|
|
6978
|
+
const places = modelPlaces(env), found = places.find((p) => hasModel(p.dir));
|
|
6959
6979
|
if (found) {
|
|
6960
6980
|
told(modelInfo(found.dir), found.why);
|
|
6961
6981
|
say("Nothing to fetch.");
|
|
@@ -6977,7 +6997,7 @@ async function modelCommand(from, say = console.log, env = process.env, sources
|
|
|
6977
6997
|
return 1;
|
|
6978
6998
|
}
|
|
6979
6999
|
}
|
|
6980
|
-
var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel
|
|
7000
|
+
var NAME$1, MODEL, RELEASE, PACKAGE, HUB, FILES, sha, userModelsDir, userModelDir, hasModel, ModelFetchError, modelFiles;
|
|
6981
7001
|
var init_model = __esmMin((() => {
|
|
6982
7002
|
init_src();
|
|
6983
7003
|
NAME$1 = "granite-embedding-small-english-r2";
|
|
@@ -7010,7 +7030,7 @@ var init_model = __esmMin((() => {
|
|
|
7010
7030
|
sha = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
7011
7031
|
userModelsDir = (env = process.env) => join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "isorite", "models");
|
|
7012
7032
|
userModelDir = (env = process.env) => join(userModelsDir(env), NAME$1);
|
|
7013
|
-
hasModel
|
|
7033
|
+
hasModel = (dir) => existsSync(join(dir, "model.onnx")) && existsSync(join(dir, "tokenizer.json"));
|
|
7014
7034
|
ModelFetchError = class extends Error {
|
|
7015
7035
|
tried;
|
|
7016
7036
|
constructor(tried) {
|
|
@@ -7145,16 +7165,17 @@ function gatewayHttp(provider, tls, env = process.env) {
|
|
|
7145
7165
|
}
|
|
7146
7166
|
/** What OpenCode may ask of the provider through isorite. */
|
|
7147
7167
|
const RELAYED = /* @__PURE__ */ new Set(["/chat/completions", "/models"]);
|
|
7148
|
-
/** The provider at /api/gateway: each request is sent on with what `endpoint` adds (the key, where the provider
|
|
7168
|
+
/** The provider at /api/gateway, as `live` has it: each request is sent on with what `endpoint` adds (the key, where the provider
|
|
7149
7169
|
* wants it, its headers and query) by `http`, and the reply streamed back. A provider that can't be reached is
|
|
7150
7170
|
* answered as an OpenAI-compatible one answers an error, so OpenCode shows why. */
|
|
7151
|
-
function relay(
|
|
7171
|
+
function relay(live, key) {
|
|
7152
7172
|
const json = (status, message) => new Response(JSON.stringify({ error: { message } }), {
|
|
7153
7173
|
status,
|
|
7154
7174
|
headers: { "Content-Type": "application/json" }
|
|
7155
7175
|
});
|
|
7156
7176
|
return async (path, req) => {
|
|
7157
7177
|
if (!RELAYED.has(path)) return json(404, `isorite relays only ${[...RELAYED].join(" and ")}`);
|
|
7178
|
+
const { provider, http } = live();
|
|
7158
7179
|
const to = endpoint(provider, await key(), path), type = req.headers.get("content-type");
|
|
7159
7180
|
const body = req.method === "POST" ? await req.text() : void 0;
|
|
7160
7181
|
let res;
|
|
@@ -7641,6 +7662,35 @@ const Revised = z.object({
|
|
|
7641
7662
|
text: z.union([z.string(), z.array(z.string()).max(40)])
|
|
7642
7663
|
}).strict();
|
|
7643
7664
|
const Key = z.object({ key: z.string().nullable() }).strict();
|
|
7665
|
+
const Pair = z.object({
|
|
7666
|
+
name: z.string().max(200),
|
|
7667
|
+
value: z.string().max(4e3),
|
|
7668
|
+
held: z.boolean().optional()
|
|
7669
|
+
}).strict();
|
|
7670
|
+
const Path = z.string().max(1e3);
|
|
7671
|
+
const SetupDraft = z.object({
|
|
7672
|
+
kind: z.enum(["openrouter", "gateway"]),
|
|
7673
|
+
name: z.string().max(100),
|
|
7674
|
+
url: z.string().max(2e3),
|
|
7675
|
+
keyHeld: z.boolean(),
|
|
7676
|
+
keyHeader: z.string().max(200),
|
|
7677
|
+
headers: z.array(Pair).max(40),
|
|
7678
|
+
query: z.array(Pair).max(40),
|
|
7679
|
+
pfx: Path,
|
|
7680
|
+
passphraseHeld: z.boolean(),
|
|
7681
|
+
cert: Path,
|
|
7682
|
+
certKey: Path,
|
|
7683
|
+
ca: Path,
|
|
7684
|
+
streamUsage: z.boolean(),
|
|
7685
|
+
maxTokens: z.number().int().positive().max(1e6).nullable(),
|
|
7686
|
+
tiers: z.object({
|
|
7687
|
+
low: z.string().max(200),
|
|
7688
|
+
mid: z.string().max(200),
|
|
7689
|
+
max: z.string().max(200)
|
|
7690
|
+
}).strict(),
|
|
7691
|
+
key: z.string().max(400).nullable().optional(),
|
|
7692
|
+
passphrase: z.string().max(1e3).nullable().optional()
|
|
7693
|
+
}).strict();
|
|
7644
7694
|
const AgentName = z.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/);
|
|
7645
7695
|
const AgentLook = z.object({
|
|
7646
7696
|
agent: AgentName,
|
|
@@ -7666,7 +7716,7 @@ const AgentStatus = z.object({
|
|
|
7666
7716
|
session: z.string().min(1).max(200),
|
|
7667
7717
|
busy: z.boolean()
|
|
7668
7718
|
}).strict();
|
|
7669
|
-
function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway }) {
|
|
7719
|
+
function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal, gateway, setup }) {
|
|
7670
7720
|
const app = new Hono();
|
|
7671
7721
|
const allowed = new Set([...allowHosts].map((h) => h.toLowerCase()));
|
|
7672
7722
|
const reply = (c, status, value) => c.body(JSON.stringify(value), status, {
|
|
@@ -7681,6 +7731,16 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
|
|
|
7681
7731
|
} catch {}
|
|
7682
7732
|
return !loopback || LOOPBACK.has(host) || host.endsWith(".ts.net") || allowed.has(host);
|
|
7683
7733
|
};
|
|
7734
|
+
/** Asked from this machine itself: its own name, and (where the connection says) from its own address. A
|
|
7735
|
+
* tailnet's name is not, even through tailscale serve, whose connections come from this machine. */
|
|
7736
|
+
const local = (c) => {
|
|
7737
|
+
let host = "";
|
|
7738
|
+
try {
|
|
7739
|
+
host = new URL("http://" + (c.req.header("host") ?? "")).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
7740
|
+
} catch {}
|
|
7741
|
+
const from = c.env?.incoming?.socket?.remoteAddress;
|
|
7742
|
+
return LOOPBACK.has(host) && (from === void 0 || /^(127\.|::1$|::ffff:127\.)/.test(from));
|
|
7743
|
+
};
|
|
7684
7744
|
const NOT_HERE = "isorite answers only to this machine's own names (localhost) and to a tailnet's (….ts.net). To be reached by another name, start it with: isorite serve <base> --allow-host <that name>";
|
|
7685
7745
|
/** Why a write is refused, or null. */
|
|
7686
7746
|
const writeRefused = (c) => {
|
|
@@ -7829,14 +7889,36 @@ function createApp({ api, token, loopback, web, allowHosts = [], notes, terminal
|
|
|
7829
7889
|
app.get("/api/context", (c) => answer(c, () => api.context(c.req.query("id") || void 0, choiceOf(c.req.query("choice")))));
|
|
7830
7890
|
post("/api/context/clear", z.object({}).strict(), () => api.clearConversation());
|
|
7831
7891
|
app.get("/api/settings", (c) => answer(c, () => api.settings()));
|
|
7832
|
-
post("/api/settings/key", Key, (_c, b) =>
|
|
7892
|
+
post("/api/settings/key", Key, async (_c, b) => {
|
|
7893
|
+
const s = await api.setKey(b.key);
|
|
7894
|
+
await setup?.changed();
|
|
7895
|
+
return api.settings().catch(() => s);
|
|
7896
|
+
}, "put");
|
|
7833
7897
|
app.get("/api/spend", (c) => answer(c, () => api.spend()));
|
|
7834
7898
|
post("/api/settings/spend-cap", z.object({ usd: z.number().positive().max(1e6).nullable() }).strict(), (_c, b) => api.setSpendCap(b.usd), "put");
|
|
7835
7899
|
post("/api/settings/connect", z.object({ callback: z.string().max(2e3) }).strict(), (_c, b) => api.connect(b.callback));
|
|
7836
7900
|
post("/api/settings/connected", z.object({
|
|
7837
7901
|
code: z.string().min(1).max(2e3),
|
|
7838
7902
|
state: z.string().min(1).max(200)
|
|
7839
|
-
}).strict(), (_c, b) =>
|
|
7903
|
+
}).strict(), async (_c, b) => {
|
|
7904
|
+
const s = await api.connected(b.code, b.state);
|
|
7905
|
+
await setup?.changed();
|
|
7906
|
+
return api.settings().catch(() => s);
|
|
7907
|
+
});
|
|
7908
|
+
if (setup) {
|
|
7909
|
+
const FROM_HERE = "The provider's setup is changed only from this machine (localhost): the key goes where it says.";
|
|
7910
|
+
const here = (c) => {
|
|
7911
|
+
if (!local(c)) throw new EngineError(FROM_HERE, 403);
|
|
7912
|
+
};
|
|
7913
|
+
app.get("/api/settings/provider", (c) => answer(c, async () => setup.view(local(c))));
|
|
7914
|
+
post("/api/settings/provider", SetupDraft, (c, b) => {
|
|
7915
|
+
here(c);
|
|
7916
|
+
return setup.keep(b, true);
|
|
7917
|
+
}, "put");
|
|
7918
|
+
post("/api/settings/provider/check", z.object({ draft: SetupDraft.nullable() }).strict(), (c, b) => setup.check(local(c) ? b.draft : null));
|
|
7919
|
+
post("/api/settings/provider/models", z.object({ draft: SetupDraft.nullable() }).strict(), async (c, b) => ({ models: await setup.models(local(c) ? b.draft : null) }));
|
|
7920
|
+
post("/api/settings/report", z.object({}).strict(), async () => ({ text: await setup.report() }));
|
|
7921
|
+
}
|
|
7840
7922
|
app.get("/api/agent/tools", (c) => answer(c, () => api.agentTools()));
|
|
7841
7923
|
post("/api/agent/look", AgentLook, (_c, b) => api.agentLook(b));
|
|
7842
7924
|
app.get("/api/agent/base", (c) => answer(c, async () => ({ notes: notes ?? null })));
|
|
@@ -8037,6 +8119,251 @@ var NodeFiles = class {
|
|
|
8037
8119
|
}
|
|
8038
8120
|
};
|
|
8039
8121
|
//#endregion
|
|
8122
|
+
//#region packages/server/src/setup.ts
|
|
8123
|
+
init_src();
|
|
8124
|
+
/** What the page sent that cannot be kept: said back to it as it is (400). */
|
|
8125
|
+
var SetupError = class extends EngineError {};
|
|
8126
|
+
/** The fields of [provider] the form holds. The rest (spend_cap, cache_marks, context, …) are left as they are. */
|
|
8127
|
+
const FORM = [
|
|
8128
|
+
"url",
|
|
8129
|
+
"name",
|
|
8130
|
+
"auth",
|
|
8131
|
+
"key",
|
|
8132
|
+
"key_header",
|
|
8133
|
+
"headers",
|
|
8134
|
+
"query",
|
|
8135
|
+
"pfx",
|
|
8136
|
+
"pfx_passphrase",
|
|
8137
|
+
"cert",
|
|
8138
|
+
"cert_key",
|
|
8139
|
+
"ca",
|
|
8140
|
+
"stream_usage",
|
|
8141
|
+
"max_tokens"
|
|
8142
|
+
];
|
|
8143
|
+
const NAMED = /^\$[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8144
|
+
const text = (v) => typeof v === "string" || typeof v === "number" ? String(v) : "";
|
|
8145
|
+
const secretPair = (name, value) => SECRET.test(name) && !NAMED.test(value);
|
|
8146
|
+
/** The setup a configuration holds, its secrets held back. */
|
|
8147
|
+
function readSetup(config) {
|
|
8148
|
+
const p = table(config.provider), tiers = table(config.tiers);
|
|
8149
|
+
const pairs = (v) => Object.entries(table(v)).map(([name, x]) => secretPair(name, text(x)) ? {
|
|
8150
|
+
name,
|
|
8151
|
+
value: "",
|
|
8152
|
+
held: true
|
|
8153
|
+
} : {
|
|
8154
|
+
name,
|
|
8155
|
+
value: text(x)
|
|
8156
|
+
});
|
|
8157
|
+
const gateway = typeof p.url === "string" && p.url !== "";
|
|
8158
|
+
return {
|
|
8159
|
+
kind: gateway ? "gateway" : "openrouter",
|
|
8160
|
+
name: text(p.name),
|
|
8161
|
+
url: gateway ? p.url : OPENROUTER.url,
|
|
8162
|
+
keyHeld: text(p.key) !== "",
|
|
8163
|
+
keyHeader: text(p.key_header),
|
|
8164
|
+
headers: pairs(p.headers),
|
|
8165
|
+
query: pairs(p.query),
|
|
8166
|
+
pfx: text(p.pfx),
|
|
8167
|
+
passphraseHeld: p.pfx_passphrase !== void 0 && text(p.pfx_passphrase) !== "",
|
|
8168
|
+
cert: text(p.cert),
|
|
8169
|
+
certKey: text(p.cert_key),
|
|
8170
|
+
ca: text(p.ca),
|
|
8171
|
+
streamUsage: p.stream_usage !== false,
|
|
8172
|
+
maxTokens: typeof p.max_tokens === "number" && p.max_tokens > 0 ? p.max_tokens : null,
|
|
8173
|
+
tiers: Object.fromEntries(TIERS.map((t) => [t, text(tiers[t]) || OPENROUTER.tiers[t]]))
|
|
8174
|
+
};
|
|
8175
|
+
}
|
|
8176
|
+
/** The configuration with `draft` in place of its setup; and whether the key was forgotten for a new address. */
|
|
8177
|
+
function applySetup(config, draft) {
|
|
8178
|
+
const old = table(config.provider), p = Object.fromEntries(Object.entries(old).filter(([k]) => !FORM.includes(k)));
|
|
8179
|
+
const gateway = draft.kind === "gateway", url = gateway ? draft.url.trim() : "";
|
|
8180
|
+
if (gateway) {
|
|
8181
|
+
let parsed;
|
|
8182
|
+
try {
|
|
8183
|
+
parsed = new URL(url);
|
|
8184
|
+
} catch {
|
|
8185
|
+
throw new SetupError("the gateway's address is not an address: it begins https:// (or http:// on this machine)");
|
|
8186
|
+
}
|
|
8187
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new SetupError("the gateway's address begins https:// (or http:// on this machine)");
|
|
8188
|
+
p.url = url.replace(/\/+$/, "");
|
|
8189
|
+
if (draft.name.trim()) p.name = draft.name.trim();
|
|
8190
|
+
}
|
|
8191
|
+
const at = (url) => (url || OPENROUTER.url).replace(/\/+$/, "");
|
|
8192
|
+
const moved = at(text(old.url)) !== at(text(p.url));
|
|
8193
|
+
if (!moved && old.auth !== void 0) p.auth = old.auth;
|
|
8194
|
+
let forgot = false;
|
|
8195
|
+
if (typeof draft.key === "string" && draft.key.trim()) {
|
|
8196
|
+
const key = draft.key.trim();
|
|
8197
|
+
if (key.length > 400 || /\s/.test(key)) throw new SetupError("that does not look like a key");
|
|
8198
|
+
p.key = key;
|
|
8199
|
+
} else if (draft.key === void 0 && old.key !== void 0) {
|
|
8200
|
+
if (moved) forgot = true;
|
|
8201
|
+
else p.key = old.key;
|
|
8202
|
+
}
|
|
8203
|
+
if (gateway) {
|
|
8204
|
+
if (draft.keyHeader.trim()) p.key_header = draft.keyHeader.trim();
|
|
8205
|
+
const pairs = (field, given) => {
|
|
8206
|
+
const before = table(old[field]), out = {};
|
|
8207
|
+
for (const pair of given) {
|
|
8208
|
+
const name = pair.name.trim();
|
|
8209
|
+
if (!name) continue;
|
|
8210
|
+
if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) throw new SetupError(`${field === "headers" ? "a header's" : "a query value's"} name has a character it cannot have: ${name}`);
|
|
8211
|
+
if (pair.held && !pair.value) {
|
|
8212
|
+
if (before[name] !== void 0 && !moved) out[name] = before[name];
|
|
8213
|
+
continue;
|
|
8214
|
+
}
|
|
8215
|
+
out[name] = pair.value;
|
|
8216
|
+
}
|
|
8217
|
+
if (Object.keys(out).length) p[field] = out;
|
|
8218
|
+
};
|
|
8219
|
+
pairs("headers", draft.headers);
|
|
8220
|
+
pairs("query", draft.query);
|
|
8221
|
+
for (const [field, value] of [
|
|
8222
|
+
["pfx", draft.pfx],
|
|
8223
|
+
["cert", draft.cert],
|
|
8224
|
+
["cert_key", draft.certKey],
|
|
8225
|
+
["ca", draft.ca]
|
|
8226
|
+
]) if (value.trim()) p[field] = value.trim();
|
|
8227
|
+
if (p.pfx !== void 0) {
|
|
8228
|
+
if (typeof draft.passphrase === "string" && draft.passphrase) p.pfx_passphrase = draft.passphrase;
|
|
8229
|
+
else if (draft.passphrase === void 0 && old.pfx_passphrase !== void 0) p.pfx_passphrase = old.pfx_passphrase;
|
|
8230
|
+
}
|
|
8231
|
+
if (!draft.streamUsage) p.stream_usage = false;
|
|
8232
|
+
if (draft.maxTokens !== null) {
|
|
8233
|
+
if (!(Number.isInteger(draft.maxTokens) && draft.maxTokens > 0 && draft.maxTokens <= 1e6)) throw new SetupError("max_tokens is a whole number of tokens");
|
|
8234
|
+
p.max_tokens = draft.maxTokens;
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
8237
|
+
const tiers = Object.fromEntries(Object.entries(table(config.tiers)).filter(([k]) => !TIERS.includes(k)));
|
|
8238
|
+
for (const t of TIERS) {
|
|
8239
|
+
const model = draft.tiers[t]?.trim() ?? "";
|
|
8240
|
+
if (model.length > 200 || /\s/.test(model)) throw new SetupError(`the ${t} tier's model is not a model's name`);
|
|
8241
|
+
if (model && (gateway || model !== OPENROUTER.tiers[t])) tiers[t] = model;
|
|
8242
|
+
}
|
|
8243
|
+
const out = {
|
|
8244
|
+
...config,
|
|
8245
|
+
provider: p
|
|
8246
|
+
};
|
|
8247
|
+
if (Object.keys(tiers).length) out.tiers = tiers;
|
|
8248
|
+
else delete out.tiers;
|
|
8249
|
+
if (!Object.keys(p).length) delete out.provider;
|
|
8250
|
+
return {
|
|
8251
|
+
config: out,
|
|
8252
|
+
forgot
|
|
8253
|
+
};
|
|
8254
|
+
}
|
|
8255
|
+
function reach(config, dir, env = process.env) {
|
|
8256
|
+
const provider = providerFrom(config, env), tls = gatewayTlsFrom(config, dir, env);
|
|
8257
|
+
return {
|
|
8258
|
+
provider,
|
|
8259
|
+
http: gatewayHttp(provider, tls, env),
|
|
8260
|
+
key: text(table(config.provider).key) || null,
|
|
8261
|
+
tls
|
|
8262
|
+
};
|
|
8263
|
+
}
|
|
8264
|
+
/** The setup, for the server's routes. */
|
|
8265
|
+
var Setup = class {
|
|
8266
|
+
path;
|
|
8267
|
+
env;
|
|
8268
|
+
o;
|
|
8269
|
+
constructor(o = {}) {
|
|
8270
|
+
this.o = o;
|
|
8271
|
+
this.env = o.env ?? process.env;
|
|
8272
|
+
this.path = o.config ?? configPath(this.env);
|
|
8273
|
+
}
|
|
8274
|
+
view(local) {
|
|
8275
|
+
return {
|
|
8276
|
+
setup: readSetup(readConfig(this.path)),
|
|
8277
|
+
local,
|
|
8278
|
+
config: this.path
|
|
8279
|
+
};
|
|
8280
|
+
}
|
|
8281
|
+
/** The configuration a draft would make, or the one kept (null). */
|
|
8282
|
+
draft(draft) {
|
|
8283
|
+
const config = readConfig(this.path);
|
|
8284
|
+
return draft ? applySetup(config, draft).config : config;
|
|
8285
|
+
}
|
|
8286
|
+
/** Doctor's checks of the provider, on the draft as if it were kept. Nothing is written. */
|
|
8287
|
+
async check(draft) {
|
|
8288
|
+
const checks = [], say = (step, outcome, said) => void checks.push({
|
|
8289
|
+
step,
|
|
8290
|
+
outcome,
|
|
8291
|
+
said
|
|
8292
|
+
});
|
|
8293
|
+
let r;
|
|
8294
|
+
try {
|
|
8295
|
+
r = reach(this.draft(draft), dirname(this.path), this.env);
|
|
8296
|
+
} catch (e) {
|
|
8297
|
+
say("setup", "fail", e.message);
|
|
8298
|
+
return checks;
|
|
8299
|
+
}
|
|
8300
|
+
if (!r.key && !r.tls && /\/\/(?:[^/]*\.)?openrouter\.ai\b/.test(r.provider.url)) {
|
|
8301
|
+
say("key", "fail", "none: paste one, or sign in for one");
|
|
8302
|
+
return checks;
|
|
8303
|
+
}
|
|
8304
|
+
await checkProvider(r.provider, r.http, r.key, say);
|
|
8305
|
+
return checks;
|
|
8306
|
+
}
|
|
8307
|
+
/** The models the provider lists, for the tiers to be chosen from. Empty where it has no list. */
|
|
8308
|
+
async models(draft) {
|
|
8309
|
+
let r;
|
|
8310
|
+
try {
|
|
8311
|
+
r = reach(this.draft(draft), dirname(this.path), this.env);
|
|
8312
|
+
} catch (e) {
|
|
8313
|
+
throw new SetupError(e.message);
|
|
8314
|
+
}
|
|
8315
|
+
const to = endpoint(r.provider, r.key, "/models");
|
|
8316
|
+
let res;
|
|
8317
|
+
try {
|
|
8318
|
+
res = await r.http.fetch(to.url, {
|
|
8319
|
+
headers: to.headers,
|
|
8320
|
+
signal: AbortSignal.timeout(2e4)
|
|
8321
|
+
});
|
|
8322
|
+
} catch (e) {
|
|
8323
|
+
throw new SetupError(unreachable(e, r.provider.url));
|
|
8324
|
+
}
|
|
8325
|
+
if (!res.ok) {
|
|
8326
|
+
await res.body?.cancel();
|
|
8327
|
+
return [];
|
|
8328
|
+
}
|
|
8329
|
+
const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id).filter((id) => typeof id === "string");
|
|
8330
|
+
return [...new Set(listed)].sort();
|
|
8331
|
+
}
|
|
8332
|
+
/** Keep the draft, once it is known to be usable, and take it into use. The file as it was is kept beside it the
|
|
8333
|
+
* first time, since writing it loses its comments. */
|
|
8334
|
+
async keep(draft, local) {
|
|
8335
|
+
const { config } = applySetup(readConfig(this.path), draft);
|
|
8336
|
+
try {
|
|
8337
|
+
reach(config, dirname(this.path), this.env);
|
|
8338
|
+
} catch (e) {
|
|
8339
|
+
throw new SetupError(e.message);
|
|
8340
|
+
}
|
|
8341
|
+
if (existsSync(this.path) && !existsSync(`${this.path}.bak`)) copyFileSync(this.path, `${this.path}.bak`);
|
|
8342
|
+
writeConfig(config, this.path);
|
|
8343
|
+
await this.o.kept?.();
|
|
8344
|
+
return this.view(local);
|
|
8345
|
+
}
|
|
8346
|
+
/** A key was kept or forgotten by the engine: taken into use as a setup is. */
|
|
8347
|
+
async changed() {
|
|
8348
|
+
await this.o.kept?.();
|
|
8349
|
+
}
|
|
8350
|
+
/** isorite doctor --report, as one block with its secrets hidden. */
|
|
8351
|
+
async report() {
|
|
8352
|
+
const lines = [];
|
|
8353
|
+
await doctor(this.o.notes, {
|
|
8354
|
+
report: true,
|
|
8355
|
+
env: this.env,
|
|
8356
|
+
out: (l) => lines.push(l),
|
|
8357
|
+
...this.o.version ? { version: this.o.version } : {},
|
|
8358
|
+
...this.o.port ? { port: this.o.port } : {}
|
|
8359
|
+
});
|
|
8360
|
+
return lines.join("\n");
|
|
8361
|
+
}
|
|
8362
|
+
};
|
|
8363
|
+
//#endregion
|
|
8364
|
+
//#region packages/server/src/version.ts
|
|
8365
|
+
const VERSION = "0.6.0";
|
|
8366
|
+
//#endregion
|
|
8040
8367
|
//#region packages/server/src/studybase.ts
|
|
8041
8368
|
init_src$1();
|
|
8042
8369
|
function git(cwd, ...args) {
|
|
@@ -8131,20 +8458,49 @@ async function openEngine({ notes }, device = deviceId(), config = configPath(),
|
|
|
8131
8458
|
} : {}
|
|
8132
8459
|
});
|
|
8133
8460
|
}
|
|
8461
|
+
/** The guide's finder, as a provider: no key, no sign-in, and a name that says what it is. */
|
|
8462
|
+
const { auth: _auth, ...openrouter } = OPENROUTER;
|
|
8463
|
+
const FINDER = {
|
|
8464
|
+
...openrouter,
|
|
8465
|
+
name: "finder",
|
|
8466
|
+
url: "http://finder.invalid/v1"
|
|
8467
|
+
};
|
|
8468
|
+
/** The provider as the configuration has it now: kept from the settings (M18), it is taken into use at once, by
|
|
8469
|
+
* Iso and the relay. With `stand`, that is asked while there is no model here. */
|
|
8470
|
+
async function liveProvider(config, stand) {
|
|
8471
|
+
if (stand && !await hasModel$1(config)) return {
|
|
8472
|
+
provider: FINDER,
|
|
8473
|
+
http: stand
|
|
8474
|
+
};
|
|
8475
|
+
const provider = providerConfig(config);
|
|
8476
|
+
return {
|
|
8477
|
+
provider,
|
|
8478
|
+
http: gatewayHttp(provider, gatewayTls(config))
|
|
8479
|
+
};
|
|
8480
|
+
}
|
|
8134
8481
|
/** A link most terminals let you click (OSC 8), when the output is a terminal; the address as it is otherwise. */
|
|
8135
8482
|
const link$1 = (url, tty = Boolean(process.stdout.isTTY)) => tty ? `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\` : url;
|
|
8136
8483
|
/** `stand`: in place of the provider, an answerer of its own, as the guide's finder is when no model is set up
|
|
8137
|
-
* (M16). It
|
|
8484
|
+
* (M16). It is asked while there is no model here (no key, no provider of your own), and needs no key. */
|
|
8138
8485
|
async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [], stand } = {}) {
|
|
8139
8486
|
const opened = locate(path);
|
|
8140
|
-
const config = configPath()
|
|
8141
|
-
const
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
const
|
|
8147
|
-
|
|
8487
|
+
const config = configPath();
|
|
8488
|
+
const current = () => liveProvider(config, stand);
|
|
8489
|
+
let live = await current();
|
|
8490
|
+
const secrets = new ConfigSecrets(config);
|
|
8491
|
+
const api = await openEngine(opened, deviceId(config), config, live.http, modelDir(), secrets);
|
|
8492
|
+
api.setProvider(live.provider, live.http);
|
|
8493
|
+
const setup = new Setup({
|
|
8494
|
+
config,
|
|
8495
|
+
notes: opened.notes,
|
|
8496
|
+
version: VERSION,
|
|
8497
|
+
port,
|
|
8498
|
+
kept: async () => {
|
|
8499
|
+
live = await current();
|
|
8500
|
+
api.setProvider(live.provider, live.http);
|
|
8501
|
+
}
|
|
8502
|
+
});
|
|
8503
|
+
const gateway = LOOPBACK.has(host) ? relay(() => live, () => secrets.get(KEY)) : void 0;
|
|
8148
8504
|
const web = existsSync(join(WEB_DIR, "index.html")) ? WEB_DIR : null;
|
|
8149
8505
|
const terminal = new Terminal();
|
|
8150
8506
|
const app = createApp({
|
|
@@ -8155,7 +8511,8 @@ async function serve$1(path, { host = "127.0.0.1", port = 4767, allowHosts = [],
|
|
|
8155
8511
|
allowHosts,
|
|
8156
8512
|
notes: opened.notes,
|
|
8157
8513
|
terminal,
|
|
8158
|
-
gateway
|
|
8514
|
+
gateway,
|
|
8515
|
+
setup
|
|
8159
8516
|
});
|
|
8160
8517
|
const server = serve({
|
|
8161
8518
|
fetch: app.fetch,
|
|
@@ -8502,6 +8859,65 @@ const TOOL = {
|
|
|
8502
8859
|
}
|
|
8503
8860
|
}
|
|
8504
8861
|
};
|
|
8862
|
+
/** The provider asked, as doctor asks it and as the page's check does (M18): its list of models, a short reply from
|
|
8863
|
+
* each tier's model, and one tool call. A few hundred tokens in all. */
|
|
8864
|
+
async function checkProvider(p, h, key, say) {
|
|
8865
|
+
const models = [.../* @__PURE__ */ new Set([
|
|
8866
|
+
p.tiers.low,
|
|
8867
|
+
p.tiers.mid,
|
|
8868
|
+
p.tiers.max
|
|
8869
|
+
])];
|
|
8870
|
+
let reached = true;
|
|
8871
|
+
try {
|
|
8872
|
+
const to = endpoint(p, key, "/models");
|
|
8873
|
+
const res = await h.fetch(to.url, {
|
|
8874
|
+
headers: to.headers,
|
|
8875
|
+
signal: AbortSignal.timeout(2e4)
|
|
8876
|
+
});
|
|
8877
|
+
if (res.ok) {
|
|
8878
|
+
const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id ?? "");
|
|
8879
|
+
const missing = models.filter((m) => !listed.includes(m));
|
|
8880
|
+
say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
|
|
8881
|
+
if (listed.length && missing.length) say("models", "fail", `${missing.join(", ")} not among them; it has ${listed.slice(0, 12).join(", ")}${listed.length > 12 ? ", …" : ""}. Name its models under [tiers]`);
|
|
8882
|
+
} else say("reached", res.status === 401 || res.status === 403 ? "fail" : "ok", res.status === 401 || res.status === 403 ? `it said ${res.status} to the list of models: the key${p.keyHeader ? "" : ", or the header it goes in (key_header)"}, or a header it wants (headers), is not accepted` : `it answered (${res.status} for the list of models, which some gateways don't have)`);
|
|
8883
|
+
} catch (e) {
|
|
8884
|
+
reached = false;
|
|
8885
|
+
say("reached", "fail", unreachable(e, p.url));
|
|
8886
|
+
}
|
|
8887
|
+
if (reached) {
|
|
8888
|
+
for (const model of models) try {
|
|
8889
|
+
const reply = await complete(h, p, key, {
|
|
8890
|
+
model,
|
|
8891
|
+
maxTokens: 16,
|
|
8892
|
+
messages: [{
|
|
8893
|
+
role: "user",
|
|
8894
|
+
content: "Reply with the one word: ready"
|
|
8895
|
+
}],
|
|
8896
|
+
signal: AbortSignal.timeout(6e4)
|
|
8897
|
+
});
|
|
8898
|
+
const usage = reply.usage.tokensIn + reply.usage.tokensOut > 0;
|
|
8899
|
+
say(`reply from ${model}`, usage || p.streamUsage === false ? "ok" : "warn", `"${reply.text.trim().slice(0, 40)}"${usage ? `, ${reply.usage.tokensIn} tokens in and ${reply.usage.tokensOut} out` : p.streamUsage === false ? "" : ": no usage reported, so the gauge can't count it"}`);
|
|
8900
|
+
} catch (e) {
|
|
8901
|
+
const said = e.message;
|
|
8902
|
+
say(`reply from ${model}`, "fail", /stream_options/i.test(said) ? `${said} Set stream_usage = false under [provider].` : said);
|
|
8903
|
+
}
|
|
8904
|
+
try {
|
|
8905
|
+
const reply = await complete(h, p, key, {
|
|
8906
|
+
model: p.tiers.mid,
|
|
8907
|
+
maxTokens: 64,
|
|
8908
|
+
tools: [TOOL],
|
|
8909
|
+
messages: [{
|
|
8910
|
+
role: "user",
|
|
8911
|
+
content: "Ping, using the ping tool."
|
|
8912
|
+
}],
|
|
8913
|
+
signal: AbortSignal.timeout(6e4)
|
|
8914
|
+
});
|
|
8915
|
+
say("tools", reply.calls.length ? "ok" : "fail", reply.calls.length ? `${p.tiers.mid} called ${reply.calls[0].name}` : `${p.tiers.mid} answered without calling the tool: Iso and the terminal need a model, and a gateway, that pass tools on`);
|
|
8916
|
+
} catch (e) {
|
|
8917
|
+
say("tools", "fail", e instanceof ModelError ? e.message : String(e));
|
|
8918
|
+
}
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8505
8921
|
/** Run the checks; 0 when none failed. */
|
|
8506
8922
|
async function doctor(folder, o = {}) {
|
|
8507
8923
|
const env = o.env ?? process.env, out = o.out ?? console.log, checks = [];
|
|
@@ -8515,7 +8931,7 @@ async function doctor(folder, o = {}) {
|
|
|
8515
8931
|
};
|
|
8516
8932
|
const path = configPath(env), execArgv = o.execArgv ?? process.execArgv;
|
|
8517
8933
|
let provider = null, http = o.http ?? null, key = null;
|
|
8518
|
-
if (!existsSync(path)) say("config", "warn", `there is no ${path}: Iso uses OpenRouter, and needs a key (
|
|
8934
|
+
if (!existsSync(path)) say("config", "warn", `there is no ${path}: Iso uses OpenRouter, and needs a key (in Iso's settings, or key under [provider] there)`);
|
|
8519
8935
|
try {
|
|
8520
8936
|
readConfig(path);
|
|
8521
8937
|
provider = providerConfig(path, env);
|
|
@@ -8523,7 +8939,7 @@ async function doctor(folder, o = {}) {
|
|
|
8523
8939
|
const tls = gatewayTls(path, env);
|
|
8524
8940
|
http ??= gatewayHttp(provider, tls, env);
|
|
8525
8941
|
if (existsSync(path)) say("config", "ok", `${path}: ${provider.name} at ${provider.url}`);
|
|
8526
|
-
say("key", key ? "ok" : tls ? "warn" : "fail", key ? `${hidden(key)}, sent ${provider.keyHeader ? `in ${provider.keyHeader}` : "as Authorization: Bearer"}` : tls ? "none; the client certificate alone is sent" : "none: set one in settings
|
|
8942
|
+
say("key", key ? "ok" : tls ? "warn" : "fail", key ? `${hidden(key)}, sent ${provider.keyHeader ? `in ${provider.keyHeader}` : "as Authorization: Bearer"}` : tls ? "none; the client certificate alone is sent" : "none: set one in Iso's settings, or as key under [provider]");
|
|
8527
8943
|
if (tls) say("certificate", "ok", `${tls.files.join(", ")}${tls.pfx || tls.cert ? "" : " (an authority only, no client certificate)"}`);
|
|
8528
8944
|
const tiers = `low ${provider.tiers.low}, mid ${provider.tiers.mid}, max ${provider.tiers.max}`;
|
|
8529
8945
|
say("models", provider.defaultTiers ? "warn" : "ok", provider.defaultTiers ? `${tiers}: OpenRouter's names, kept because [tiers] names none for ${provider.name}; a gateway usually calls its models otherwise` : tiers);
|
|
@@ -8547,56 +8963,7 @@ async function doctor(folder, o = {}) {
|
|
|
8547
8963
|
p.tiers.max
|
|
8548
8964
|
])];
|
|
8549
8965
|
if (!o.report) out(` asking ${p.name}: its models, then ${models.length + 1} replies of a few words (a few hundred tokens in all)`);
|
|
8550
|
-
|
|
8551
|
-
try {
|
|
8552
|
-
const to = endpoint(p, key, "/models");
|
|
8553
|
-
const res = await h.fetch(to.url, {
|
|
8554
|
-
headers: to.headers,
|
|
8555
|
-
signal: AbortSignal.timeout(2e4)
|
|
8556
|
-
});
|
|
8557
|
-
if (res.ok) {
|
|
8558
|
-
const listed = ((await res.json().catch(() => ({}))).data ?? []).map((m) => m.id ?? "");
|
|
8559
|
-
const missing = models.filter((m) => !listed.includes(m));
|
|
8560
|
-
say("reached", "ok", `${to.url.replace(/\?.*$/, "")} answered with ${listed.length} models`);
|
|
8561
|
-
if (listed.length && missing.length) say("models", "fail", `${missing.join(", ")} not among them; it has ${listed.slice(0, 12).join(", ")}${listed.length > 12 ? ", …" : ""}. Name its models under [tiers]`);
|
|
8562
|
-
} else say("reached", res.status === 401 || res.status === 403 ? "fail" : "ok", res.status === 401 || res.status === 403 ? `it said ${res.status} to the list of models: the key${p.keyHeader ? "" : ", or the header it goes in (key_header)"}, or a header it wants (headers), is not accepted` : `it answered (${res.status} for the list of models, which some gateways don't have)`);
|
|
8563
|
-
} catch (e) {
|
|
8564
|
-
reached = false;
|
|
8565
|
-
say("reached", "fail", unreachable(e, p.url));
|
|
8566
|
-
}
|
|
8567
|
-
if (reached) {
|
|
8568
|
-
for (const model of models) try {
|
|
8569
|
-
const reply = await complete(h, p, key, {
|
|
8570
|
-
model,
|
|
8571
|
-
maxTokens: 16,
|
|
8572
|
-
messages: [{
|
|
8573
|
-
role: "user",
|
|
8574
|
-
content: "Reply with the one word: ready"
|
|
8575
|
-
}],
|
|
8576
|
-
signal: AbortSignal.timeout(6e4)
|
|
8577
|
-
});
|
|
8578
|
-
const usage = reply.usage.tokensIn + reply.usage.tokensOut > 0;
|
|
8579
|
-
say(`reply from ${model}`, usage || p.streamUsage === false ? "ok" : "warn", `"${reply.text.trim().slice(0, 40)}"${usage ? `, ${reply.usage.tokensIn} tokens in and ${reply.usage.tokensOut} out` : p.streamUsage === false ? "" : ": no usage reported, so the gauge can't count it"}`);
|
|
8580
|
-
} catch (e) {
|
|
8581
|
-
const said = e.message;
|
|
8582
|
-
say(`reply from ${model}`, "fail", /stream_options/i.test(said) ? `${said} Set stream_usage = false under [provider].` : said);
|
|
8583
|
-
}
|
|
8584
|
-
try {
|
|
8585
|
-
const reply = await complete(h, p, key, {
|
|
8586
|
-
model: p.tiers.mid,
|
|
8587
|
-
maxTokens: 64,
|
|
8588
|
-
tools: [TOOL],
|
|
8589
|
-
messages: [{
|
|
8590
|
-
role: "user",
|
|
8591
|
-
content: "Ping, using the ping tool."
|
|
8592
|
-
}],
|
|
8593
|
-
signal: AbortSignal.timeout(6e4)
|
|
8594
|
-
});
|
|
8595
|
-
say("tools", reply.calls.length ? "ok" : "fail", reply.calls.length ? `${p.tiers.mid} called ${reply.calls[0].name}` : `${p.tiers.mid} answered without calling the tool: Iso and the terminal need a model, and a gateway, that pass tools on`);
|
|
8596
|
-
} catch (e) {
|
|
8597
|
-
say("tools", "fail", e instanceof ModelError ? e.message : String(e));
|
|
8598
|
-
}
|
|
8599
|
-
}
|
|
8966
|
+
await checkProvider(p, h, key, say);
|
|
8600
8967
|
}
|
|
8601
8968
|
try {
|
|
8602
8969
|
const found = findOpenCode(env, o.probe);
|
|
@@ -10015,7 +10382,6 @@ function loadGuide(dir = GUIDE_DIR) {
|
|
|
10015
10382
|
}
|
|
10016
10383
|
//#endregion
|
|
10017
10384
|
//#region packages/server/src/guide.ts
|
|
10018
|
-
init_src();
|
|
10019
10385
|
init_src$1();
|
|
10020
10386
|
const GUIDE_FOLDER = join(homedir(), "isorite-guide");
|
|
10021
10387
|
/** Write the guide into `folder`, unless it is there already. Returns whether it was written now. A folder that
|
|
@@ -10037,19 +10403,15 @@ function writeGuide(folder) {
|
|
|
10037
10403
|
fresh: true
|
|
10038
10404
|
};
|
|
10039
10405
|
}
|
|
10040
|
-
/** Whether Iso has a model here: a key kept, or a provider of your own (a gateway may need no key). */
|
|
10041
|
-
async function hasModel(config = configPath()) {
|
|
10042
|
-
return await new ConfigSecrets(config).get("provider.key") !== null || providerConfig(config).url !== OPENROUTER.url;
|
|
10043
|
-
}
|
|
10044
10406
|
async function guide(folder = GUIDE_FOLDER, { port = 4767, out = console.log } = {}) {
|
|
10045
10407
|
const { notes, fresh } = writeGuide(folder);
|
|
10046
|
-
const model = await hasModel();
|
|
10408
|
+
const model = await hasModel$1();
|
|
10047
10409
|
out(fresh ? `isorite guide: written to ${notes}; it is yours to change, and run again it opens as you left it` : `isorite guide: ${notes}, as you left it`);
|
|
10048
10410
|
out(model ? " Iso: your provider's model" : " Iso: the guide's finder, which answers with the notes that match (no model is set up)");
|
|
10049
10411
|
out(` start at http://localhost:${port}/#/n/${START}`);
|
|
10050
10412
|
await serve$1(notes, {
|
|
10051
10413
|
port,
|
|
10052
|
-
|
|
10414
|
+
stand: finderHttp(16)
|
|
10053
10415
|
});
|
|
10054
10416
|
if (process.env.ISORITE_NO_BROWSER !== "1") openBrowser(`http://localhost:${port}/#/n/${START}`);
|
|
10055
10417
|
}
|
|
@@ -10126,7 +10488,6 @@ async function writeGeometry(folder, now = Date.now()) {
|
|
|
10126
10488
|
//#region packages/server/src/main.ts
|
|
10127
10489
|
init_src$1();
|
|
10128
10490
|
init_model();
|
|
10129
|
-
const VERSION = "0.5.0";
|
|
10130
10491
|
const USAGE = `isorite: an agentic harness for understanding
|
|
10131
10492
|
|
|
10132
10493
|
isorite new <folder> start a knowledge base: the folder, empty, with isorite's .isorite/
|