teapot-coding-agent 0.37.59 → 0.37.60
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/dist/acp/adapter.js +4 -72
- package/dist/agent/agent.js +52 -2246
- package/dist/agent/compact-rules.js +0 -53
- package/dist/agent/editorconfig.js +4 -56
- package/dist/agent/goal-audit.js +0 -34
- package/dist/agent/key-pool.js +0 -52
- package/dist/agent/kill.js +1 -13
- package/dist/agent/llm.js +8 -309
- package/dist/agent/mcp-oauth-manager.js +2 -255
- package/dist/agent/mcp-registry.js +1 -188
- package/dist/agent/mcp.js +1 -85
- package/dist/agent/meta-tools.js +0 -26
- package/dist/agent/plugins.js +1 -81
- package/dist/agent/provider-headers.js +1 -28
- package/dist/agent/skills.js +8 -94
- package/dist/agent/tools.js +16 -809
- package/dist/bus.js +1 -2
- package/dist/config-schema.js +0 -40
- package/dist/home.js +1 -19
- package/dist/index.js +0 -136
- package/dist/log/events.js +3 -195
- package/dist/master.js +31 -1176
- package/dist/migrations/index.js +0 -55
- package/dist/migrations/providers.js +0 -13
- package/dist/migrations/registry.js +0 -9
- package/dist/migrations/session-titles.js +3 -43
- package/dist/migrations/sessions.js +2 -20
- package/dist/model-meta.js +2 -74
- package/dist/scheduler/cron.js +2 -17
- package/dist/server/api.js +12 -778
- package/dist/server/image-resize.js +1 -64
- package/dist/server/term-size.js +1 -120
- package/dist/server/terminals.js +0 -159
- package/dist/session-title.js +2 -35
- package/package.json +1 -1
- package/public/assets/{index-KzHw4Sqo.js → index-DhJx7VDq.js} +4 -4
- package/public/assets/index-DhJx7VDq.js.map +1 -0
- package/public/assets/{shiki-BwDjoZdu.js → shiki-LBMkaWYb.js} +2 -2
- package/public/assets/shiki-LBMkaWYb.js.map +1 -0
- package/public/index.html +1 -1
- package/public/assets/index-KzHw4Sqo.js.map +0 -1
- package/public/assets/shiki-BwDjoZdu.js.map +0 -1
package/dist/migrations/index.js
CHANGED
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ordered, one-way migrations for on-disk state.
|
|
3
|
-
*
|
|
4
|
-
* ## Why order and idempotency matter more than the transformations
|
|
5
|
-
*
|
|
6
|
-
* A single upgrade path is easy. The hard case is a machine that skips several releases: an
|
|
7
|
-
* operator on v0.20 upgrading straight to v0.40 runs EVERY migration in between at once. Two
|
|
8
|
-
* migrations that each look correct can then be wrong together — the canonical example is a future
|
|
9
|
-
* `config.json` split combined with today's "store the session title in `agents[]`":
|
|
10
|
-
*
|
|
11
|
-
* split first → titles land in the new config directory, then the old file is read again and
|
|
12
|
-
* the titles are written a second time, or to the wrong file;
|
|
13
|
-
* titles first → the split then has to carry a field it never knew about.
|
|
14
|
-
*
|
|
15
|
-
* So each migration here:
|
|
16
|
-
*
|
|
17
|
-
* - is **idempotent** — running it twice is a no-op, because a crash mid-upgrade must be
|
|
18
|
-
* resumable and a re-run must not double-apply;
|
|
19
|
-
* - declares an **order** — `order` is ascending, and a later step may assume every earlier one
|
|
20
|
-
* has run;
|
|
21
|
-
* - never deletes data it does not fully replace (a rename, not a delete-then-write);
|
|
22
|
-
* - reports what it did, so a boot log shows which steps actually ran.
|
|
23
|
-
*
|
|
24
|
-
* ## What this module is NOT
|
|
25
|
-
*
|
|
26
|
-
* It does not decide WHICH migrations exist — each one lives in its own file and is registered in
|
|
27
|
-
* `index.ts`. Keeping them out of `master.ts` is the point: they were spread across the boot path,
|
|
28
|
-
* where an ordering mistake is invisible.
|
|
29
|
-
*/
|
|
30
|
-
/**
|
|
31
|
-
* Run every registered migration in `order`, skipping nothing.
|
|
32
|
-
*
|
|
33
|
-
* There is no "already ran" gate on purpose: each step is idempotent, so the cheapest correct
|
|
34
|
-
* policy is to always run them. A recorded version would have to be validated against a config the
|
|
35
|
-
* operator may have hand-edited or restored from backup, and a wrong version claim is how a
|
|
36
|
-
* migration silently does not run.
|
|
37
|
-
*/
|
|
38
1
|
export async function runMigrations(migrations, ctx) {
|
|
39
2
|
const applied = [];
|
|
40
3
|
for (const m of [...migrations].sort((a, b) => a.order - b.order)) {
|
|
@@ -45,26 +8,8 @@ export async function runMigrations(migrations, ctx) {
|
|
|
45
8
|
}
|
|
46
9
|
}
|
|
47
10
|
catch (err) {
|
|
48
|
-
// One broken step must not block the boot: the session data is still readable, and refusing
|
|
49
|
-
// to start would be worse than starting with one conversion unmade.
|
|
50
11
|
ctx.log(`migration ${m.id} FAILED (${err.message}) — continuing without it`);
|
|
51
12
|
}
|
|
52
13
|
}
|
|
53
14
|
return { applied };
|
|
54
15
|
}
|
|
55
|
-
/**
|
|
56
|
-
* ## What is deliberately NOT here
|
|
57
|
-
*
|
|
58
|
-
* Two migrations stay in `src/agent/agent.ts` because they are per-SESSION filesystem moves that
|
|
59
|
-
* need the agent's own paths, and they already satisfy the rules above:
|
|
60
|
-
*
|
|
61
|
-
* - `migrateGoalFromWorkspace` — `<workspace>/GOAL.md` → the session's goal store. Deletes the
|
|
62
|
-
* legacy file only after a successful write, so a failure re-runs.
|
|
63
|
-
* - `migrateMemoryToAgentScope` — a session's `memory.md` → the agent-wide one. APPENDS (the
|
|
64
|
-
* agent-wide file may already hold other sessions' notes), then removes the source. A missing
|
|
65
|
-
* write leaves the session copy in place.
|
|
66
|
-
*
|
|
67
|
-
* Moving them here would mean handing this module session paths it has no business knowing. What
|
|
68
|
-
* matters is that they obey the same contract: idempotent, no data destroyed that was not
|
|
69
|
-
* replaced, and ordered after the config-level steps (they run at agent start, which is later).
|
|
70
|
-
*/
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
import { migrateProviderKeys } from "../master.js";
|
|
2
|
-
/**
|
|
3
|
-
* Fold a legacy single `apiKey` into an `apiKeys` array.
|
|
4
|
-
*
|
|
5
|
-
* `apiKey` is KEPT in step with `apiKeys[0]` rather than deleted: other readers still expect it (the
|
|
6
|
-
* setup wizard, `/api/providers`, hand-edited files), and a migration that removes a field the
|
|
7
|
-
* operator can see is one they will report as data loss.
|
|
8
|
-
*
|
|
9
|
-
* Idempotent because `apiKeys` is AUTHORITATIVE once present: re-running on a migrated entry
|
|
10
|
-
* produces the same list. The single `apiKey` only SEEDS the pool when there is no list — appending
|
|
11
|
-
* it as well would resurrect a credential the operator had already replaced.
|
|
12
|
-
*/
|
|
13
2
|
export const providerKeys = {
|
|
14
3
|
order: 10,
|
|
15
4
|
id: "providers-api-keys",
|
|
@@ -25,8 +14,6 @@ export const providerKeys = {
|
|
|
25
14
|
continue;
|
|
26
15
|
const p = entry;
|
|
27
16
|
const before = JSON.stringify(p);
|
|
28
|
-
// ONE implementation of the folding rule, shared with the runtime normaliser — a second copy
|
|
29
|
-
// here is how the two drift and disagree about which key is authoritative.
|
|
30
17
|
const next = migrateProviderKeys(p);
|
|
31
18
|
for (const k of Object.keys(p))
|
|
32
19
|
delete p[k];
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { providerKeys } from "./providers.js";
|
|
2
2
|
import { flatSessionLayout } from "./sessions.js";
|
|
3
3
|
import { sessionTitles } from "./session-titles.js";
|
|
4
|
-
/**
|
|
5
|
-
* Every migration, in application order.
|
|
6
|
-
*
|
|
7
|
-
* Add new ones with a HIGHER `order` than everything here, and never renumber an existing one — a
|
|
8
|
-
* machine that already ran it would skip it under the new number.
|
|
9
|
-
*
|
|
10
|
-
* Ordering rule: a migration may assume every lower `order` has run. That is what makes a
|
|
11
|
-
* multi-release jump safe. If two migrations must not be reordered later, say so in both files.
|
|
12
|
-
*/
|
|
13
4
|
export const MIGRATIONS = [providerKeys, flatSessionLayout, sessionTitles];
|
|
14
5
|
export { runMigrations } from "./index.js";
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Derive each agent's session title ONCE and store it in `config.agents[].sessionTitle`.
|
|
5
|
-
*
|
|
6
|
-
* Titles used to be recomputed on every `allSessions()` — opening up to 830 session logs and
|
|
7
|
-
* allocating a 16KB buffer for each, MEASURED at 83.8ms, 8.1MB read and ~38MB of short-lived
|
|
8
|
-
* garbage per scan, on every `/api/sessions`. Storing it makes listing a config lookup.
|
|
9
|
-
*
|
|
10
|
-
* Runs ONCE PER SESSION rather than once per process: the check is `sessionTitle` on the agent's
|
|
11
|
-
* CURRENT session, so a machine that has already been through this does no file I/O at all. That
|
|
12
|
-
* is what makes it idempotent — and why it must come AFTER any step that could change `agents[]`.
|
|
13
|
-
*
|
|
14
|
-
* A session whose first prompt is a HARNESS prompt yields no title: sub-agent sessions receive only
|
|
15
|
-
* those and are labelled from their id, so inventing one would name a child after its parent's
|
|
16
|
-
* instruction.
|
|
17
|
-
*/
|
|
18
3
|
export const sessionTitles = {
|
|
19
4
|
order: 30,
|
|
20
5
|
id: "agents-session-title",
|
|
@@ -27,21 +12,11 @@ export const sessionTitles = {
|
|
|
27
12
|
const session = typeof a.session === "string" ? a.session : "";
|
|
28
13
|
if (!session)
|
|
29
14
|
continue;
|
|
30
|
-
// already stored (possibly hand-edited — a chosen name must never be overwritten)
|
|
31
15
|
if (typeof a.sessionTitle === "string" && a.sessionTitle.trim())
|
|
32
16
|
continue;
|
|
33
17
|
if (session.includes("/") || session.includes(".."))
|
|
34
|
-
continue;
|
|
18
|
+
continue;
|
|
35
19
|
const title = firstUserPromptText(path.join(ctx.dataDir, "sessions", session, "chat.jsonl"));
|
|
36
|
-
/**
|
|
37
|
-
* The SESSION ID is the fallback, not a marker.
|
|
38
|
-
*
|
|
39
|
-
* A sub-agent session receives only harness prompts, so no title can ever be derived from it —
|
|
40
|
-
* and a session id does not change either, so there is nothing to retry for. Writing the id
|
|
41
|
-
* makes the field ALWAYS present, which is what makes the pass idempotent without an extra
|
|
42
|
-
* flag: the next boot sees a title and skips. The sidebar already shows the id in this case,
|
|
43
|
-
* so nothing visible changes.
|
|
44
|
-
*/
|
|
45
20
|
a.sessionTitle = title ?? session;
|
|
46
21
|
changed = true;
|
|
47
22
|
}
|
|
@@ -50,19 +25,6 @@ export const sessionTitles = {
|
|
|
50
25
|
return changed;
|
|
51
26
|
},
|
|
52
27
|
};
|
|
53
|
-
/**
|
|
54
|
-
* The first USER prompt in a log, trimmed to a title. `null` when there is none.
|
|
55
|
-
*
|
|
56
|
-
* Bounded (256KB) and stops at the first prompt, so one enormous log cannot dominate boot. A torn
|
|
57
|
-
* final line at a chunk boundary is skipped rather than aborting the read.
|
|
58
|
-
*/
|
|
59
|
-
/**
|
|
60
|
-
* ONE reusable read buffer for the whole pass.
|
|
61
|
-
*
|
|
62
|
-
* `Buffer.alloc` per session is what made the first boot spike: 830 sessions x 16KB of short-lived
|
|
63
|
-
* buffer, MEASURED as heap 28.2MB → 108.5MB during `start()` (33.4MB after GC). The scan is a
|
|
64
|
-
* single-threaded loop, so sharing one buffer is safe and removes the churn entirely.
|
|
65
|
-
*/
|
|
66
28
|
let scanBuffer;
|
|
67
29
|
function firstUserPromptText(file) {
|
|
68
30
|
let fd;
|
|
@@ -89,12 +51,12 @@ function firstUserPromptText(file) {
|
|
|
89
51
|
e = JSON.parse(l);
|
|
90
52
|
}
|
|
91
53
|
catch {
|
|
92
|
-
continue;
|
|
54
|
+
continue;
|
|
93
55
|
}
|
|
94
56
|
if (e.type !== "prompt")
|
|
95
57
|
continue;
|
|
96
58
|
if (e.data?.source !== "user")
|
|
97
|
-
return null;
|
|
59
|
+
return null;
|
|
98
60
|
return titleOf(typeof e.data.text === "string" ? e.data.text : null);
|
|
99
61
|
}
|
|
100
62
|
}
|
|
@@ -109,12 +71,10 @@ function firstUserPromptText(file) {
|
|
|
109
71
|
closeSync(fd);
|
|
110
72
|
}
|
|
111
73
|
catch {
|
|
112
|
-
/* already closed */
|
|
113
74
|
}
|
|
114
75
|
}
|
|
115
76
|
}
|
|
116
77
|
}
|
|
117
|
-
/** the first usable line of a prompt, clipped — the sidebar shows one short label */
|
|
118
78
|
function titleOf(text) {
|
|
119
79
|
const line = (text ?? "")
|
|
120
80
|
.split("\n")
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Move sessions from the ≤0.5.0 flat layout (`<dataDir>/<id>.jsonl`) into
|
|
5
|
-
* `<dataDir>/sessions/<id>/chat.jsonl`.
|
|
6
|
-
*
|
|
7
|
-
* A RENAME, not a copy-then-delete: a crash between the two would otherwise leave the history in
|
|
8
|
-
* neither place. `rename` within one filesystem is atomic, so a re-run either finds the legacy file
|
|
9
|
-
* (and moves it) or finds it already moved.
|
|
10
|
-
*
|
|
11
|
-
* Order 20: it must run before anything reads a session directory.
|
|
12
|
-
*/
|
|
13
3
|
export const flatSessionLayout = {
|
|
14
4
|
order: 20,
|
|
15
5
|
id: "sessions-flat-layout",
|
|
16
6
|
summary: "sessions/<id>.jsonl → sessions/<id>/chat.jsonl",
|
|
17
7
|
run(ctx) {
|
|
18
|
-
/**
|
|
19
|
-
* Scan the DATA DIR, not `config.agents`.
|
|
20
|
-
*
|
|
21
|
-
* A legacy log can exist for an agent that is no longer in the config (archived, removed, or a
|
|
22
|
-
* config that failed to load) — and those are exactly the histories worth rescuing. Iterating
|
|
23
|
-
* the config also made this a no-op whenever the config listed no agents, which is how a test
|
|
24
|
-
* with an empty agent list silently stopped exercising it.
|
|
25
|
-
*/
|
|
26
8
|
let names;
|
|
27
9
|
try {
|
|
28
10
|
names = readdirSync(ctx.dataDir);
|
|
@@ -37,11 +19,11 @@ export const flatSessionLayout = {
|
|
|
37
19
|
continue;
|
|
38
20
|
const id = name.slice(0, -".jsonl".length);
|
|
39
21
|
if (!id || id.includes("/") || id.includes(".."))
|
|
40
|
-
continue;
|
|
22
|
+
continue;
|
|
41
23
|
const legacy = path.join(ctx.dataDir, name);
|
|
42
24
|
const target = path.join(root, id);
|
|
43
25
|
if (existsSync(target))
|
|
44
|
-
continue;
|
|
26
|
+
continue;
|
|
45
27
|
mkdirSync(target, { recursive: true });
|
|
46
28
|
renameSync(legacy, path.join(target, "chat.jsonl"));
|
|
47
29
|
ctx.log(`${name} → sessions/${id}/chat.jsonl`);
|
package/dist/model-meta.js
CHANGED
|
@@ -1,39 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider model metadata (context window sizes) with a small TTL cache.
|
|
3
|
-
* Used to auto-populate contextWindowTokens so compaction budgets derive
|
|
4
|
-
* from the real window instead of the safe default.
|
|
5
|
-
*/
|
|
6
1
|
import { providerHeaders } from "./agent/provider-headers.js";
|
|
7
2
|
const cache = new Map();
|
|
8
3
|
const inFlight = new Map();
|
|
9
4
|
const TTL = 10 * 60_000;
|
|
10
|
-
/**
|
|
11
|
-
* How long a FAILED lookup is remembered.
|
|
12
|
-
*
|
|
13
|
-
* A failure used to be dropped on the floor (the `catch` returned `[]` without
|
|
14
|
-
* caching), so a dead, unauthorised, or simply slow endpoint was re-fetched for
|
|
15
|
-
* EVERY agent pointed at it. `addAgent` awaits this twice per agent and races it
|
|
16
|
-
* against a 1.5 s timeout, so with 493 agents across a handful of providers the
|
|
17
|
-
* same failing request — and its timeout — repeated hundreds of times. MEASURED
|
|
18
|
-
* at boot: 1883 ms of the 4.6 s was idle, waiting on exactly this.
|
|
19
|
-
*
|
|
20
|
-
* Negative results expire much sooner than positive ones: a provider coming back
|
|
21
|
-
* up should be usable within a minute, not locked out for ten.
|
|
22
|
-
*/
|
|
23
5
|
const NEGATIVE_TTL = 30_000;
|
|
24
|
-
/** is this cached entry still valid? (failures expire sooner) */
|
|
25
6
|
function fresh(entry) {
|
|
26
7
|
return Date.now() - entry.at < (entry.negative ? NEGATIVE_TTL : TTL);
|
|
27
8
|
}
|
|
28
|
-
/**
|
|
29
|
-
* The catalogue if it is ALREADY known, without starting a fetch (#143).
|
|
30
|
-
*
|
|
31
|
-
* Boot uses this so it never waits for metadata it can do without: the context
|
|
32
|
-
* window only tunes the compaction budget, and `supportedParameters` only gates
|
|
33
|
-
* the reasoning-effort controls (#47). `prefetchModelLists()` starts the real
|
|
34
|
-
* lookups in parallel, so an endpoint that answers quickly is usually warm by the
|
|
35
|
-
* time its first agent is created, and a slow or dead one costs no boot time.
|
|
36
|
-
*/
|
|
37
9
|
export function peekModelList(baseUrl) {
|
|
38
10
|
const root = baseUrl.replace(/\/+$/, "");
|
|
39
11
|
const hit = cache.get(root);
|
|
@@ -43,15 +15,11 @@ export function peekModelList(baseUrl) {
|
|
|
43
15
|
return undefined;
|
|
44
16
|
return hit.negative ? [] : hit.list;
|
|
45
17
|
}
|
|
46
|
-
/** Fetch GET /models from an OpenAI-compatible endpoint (cached ~10 min). */
|
|
47
18
|
export async function fetchModelList(baseUrl, apiKey) {
|
|
48
19
|
const root = baseUrl.replace(/\/+$/, "");
|
|
49
20
|
const hit = cache.get(root);
|
|
50
21
|
if (hit && fresh(hit))
|
|
51
22
|
return hit.list;
|
|
52
|
-
// An in-flight lookup is SHARED: without this, two callers racing for the
|
|
53
|
-
// same endpoint each paid the full timeout, and `newSession` in ACP mode
|
|
54
|
-
// blocked on it before it could even reply to the editor (#15).
|
|
55
23
|
const inflight = inFlight.get(root);
|
|
56
24
|
if (inflight)
|
|
57
25
|
return inflight;
|
|
@@ -60,10 +28,8 @@ export async function fetchModelList(baseUrl, apiKey) {
|
|
|
60
28
|
const res = await fetch(`${root}/models`, {
|
|
61
29
|
headers: {
|
|
62
30
|
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
63
|
-
...providerHeaders(baseUrl),
|
|
31
|
+
...providerHeaders(baseUrl),
|
|
64
32
|
},
|
|
65
|
-
// boot-time calls happen per agent; a slow/unroutable endpoint used to
|
|
66
|
-
// stall startup for up to 15s PER AGENT before this was tightened
|
|
67
33
|
signal: AbortSignal.timeout(3_000),
|
|
68
34
|
});
|
|
69
35
|
if (!res.ok)
|
|
@@ -73,42 +39,21 @@ export async function fetchModelList(baseUrl, apiKey) {
|
|
|
73
39
|
.map((m) => ({
|
|
74
40
|
id: typeof m.id === "string" ? m.id : "",
|
|
75
41
|
contextLength: typeof m.context_length === "number" ? m.context_length : undefined,
|
|
76
|
-
// OpenRouter-style USD per token (e.g. "0.000003") — powers the
|
|
77
|
-
// runtime cost estimate. Providers without pricing stay undefined.
|
|
78
42
|
pricing: m.pricing && (m.pricing.prompt !== undefined || m.pricing.completion !== undefined)
|
|
79
43
|
? {
|
|
80
44
|
prompt: Number(m.pricing.prompt ?? 0),
|
|
81
45
|
completion: Number(m.pricing.completion ?? 0),
|
|
82
46
|
}
|
|
83
47
|
: undefined,
|
|
84
|
-
/**
|
|
85
|
-
* `modalities` is only known when the provider PUBLISHES it — in practice OpenRouter's
|
|
86
|
-
* `architecture.input_modalities`.
|
|
87
|
-
*
|
|
88
|
-
* Every other endpoint omits the field, so this stays `undefined` and the UI must treat it as
|
|
89
|
-
* UNKNOWN rather than as "text only". Reported as "Command Code GOATのモデルカタログが
|
|
90
|
-
* input/outputのフォーマットを適切に提供していない? OpenRouterしかちゃんと対応できてない可能性は
|
|
91
|
-
* ある" — which is exactly right, and the code was right not to invent a value.
|
|
92
|
-
*
|
|
93
|
-
* The consequence to know about: without a modality list nothing can decide "this model
|
|
94
|
-
* accepts an image", so an operator's screenshot reaches the model as a FILE PATH and an agent
|
|
95
|
-
* that tries `read_file` gets bytes. The guard for that lives in `read_file` (it refuses a
|
|
96
|
-
* binary file by name instead of returning mojibake), so the failure is at least legible.
|
|
97
|
-
*/
|
|
98
48
|
modalities: Array.isArray(m.architecture?.input_modalities) && m.architecture.input_modalities.length
|
|
99
49
|
? {
|
|
100
50
|
input: m.architecture.input_modalities,
|
|
101
51
|
output: m.architecture.output_modalities ?? ["text"],
|
|
102
52
|
}
|
|
103
53
|
: undefined,
|
|
104
|
-
// OpenRouter advertises per-model capabilities; other endpoints omit
|
|
105
|
-
// the field entirely, which leaves support "unknown" rather than false.
|
|
106
54
|
supportedParameters: Array.isArray(m.supported_parameters)
|
|
107
55
|
? m.supported_parameters.filter((p) => typeof p === "string")
|
|
108
56
|
: undefined,
|
|
109
|
-
// per-model reasoning levels when published (OpenRouter): the UI offers
|
|
110
|
-
// exactly these, and the request is clamped into them — offering a level the
|
|
111
|
-
// model does not have is a control that does nothing (#150)
|
|
112
57
|
supportedEfforts: Array.isArray(m.reasoning?.supported_efforts)
|
|
113
58
|
? m.reasoning.supported_efforts.filter((e) => typeof e === "string")
|
|
114
59
|
: undefined,
|
|
@@ -122,10 +67,8 @@ export async function fetchModelList(baseUrl, apiKey) {
|
|
|
122
67
|
return list;
|
|
123
68
|
}
|
|
124
69
|
catch {
|
|
125
|
-
// Remember the FAILURE (briefly). Dropping it meant every agent on this
|
|
126
|
-
// endpoint paid the fetch — and up to the 1.5 s race timeout — again.
|
|
127
70
|
cache.set(root, { at: Date.now(), list: [], negative: true });
|
|
128
|
-
return [];
|
|
71
|
+
return [];
|
|
129
72
|
}
|
|
130
73
|
})();
|
|
131
74
|
inFlight.set(root, p);
|
|
@@ -139,12 +82,10 @@ export async function fetchModelList(baseUrl, apiKey) {
|
|
|
139
82
|
export function contextLengthFor(list, model) {
|
|
140
83
|
return list.find((m) => m.id === model)?.contextLength;
|
|
141
84
|
}
|
|
142
|
-
/** Reasoning-effort levels OpenRouter accepts (`reasoning_effort`). */
|
|
143
85
|
export const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
144
86
|
export function isReasoningEffort(v) {
|
|
145
87
|
return typeof v === "string" && REASONING_EFFORTS.includes(v);
|
|
146
88
|
}
|
|
147
|
-
/** True when the endpoint is OpenRouter (the only one that documents effort). */
|
|
148
89
|
export function isOpenRouter(baseUrl) {
|
|
149
90
|
try {
|
|
150
91
|
return /(^|\.)openrouter\.ai$/i.test(new URL(String(baseUrl ?? "")).host);
|
|
@@ -153,25 +94,12 @@ export function isOpenRouter(baseUrl) {
|
|
|
153
94
|
return false;
|
|
154
95
|
}
|
|
155
96
|
}
|
|
156
|
-
/**
|
|
157
|
-
* The `reasoning_effort` value to actually SEND, or undefined to omit it.
|
|
158
|
-
*
|
|
159
|
-
* Verified against the live OpenRouter /models endpoint (462 models): 196
|
|
160
|
-
* advertise `reasoning_effort`, 331 the broader `reasoning`, and NONE a bare
|
|
161
|
-
* top-level `effort`. Support is therefore far from universal, and sending the
|
|
162
|
-
* parameter to a model that does not advertise it is how a request gets
|
|
163
|
-
* rejected or a setting silently ignored (#47) — so it is sent only when the
|
|
164
|
-
* endpoint is OpenRouter AND the model advertises it. Off by default: an absent
|
|
165
|
-
* setting means "let the provider apply its own default".
|
|
166
|
-
*/
|
|
167
97
|
export function resolveReasoningEffort(opts) {
|
|
168
98
|
if (!isReasoningEffort(opts.effort))
|
|
169
99
|
return undefined;
|
|
170
100
|
if (!isOpenRouter(opts.baseUrl))
|
|
171
101
|
return undefined;
|
|
172
102
|
const meta = opts.list?.find((m) => m.id === opts.model);
|
|
173
|
-
// No catalogue for this endpoint: we cannot verify support, so do not send.
|
|
174
|
-
// A wrong body field is worse than an unset one.
|
|
175
103
|
if (!meta?.supportedParameters)
|
|
176
104
|
return undefined;
|
|
177
105
|
if (!meta.supportedParameters.includes("reasoning_effort"))
|
package/dist/scheduler/cron.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal 5-field cron matcher (minute hour day-of-month month day-of-week).
|
|
3
|
-
* Also supports shorthand "every <n>s|m|h".
|
|
4
|
-
* The master ticks once every 15s; matching is a handful of integer compares.
|
|
5
|
-
*/
|
|
6
1
|
const BOUNDS = [
|
|
7
2
|
[0, 59],
|
|
8
3
|
[0, 23],
|
|
9
4
|
[1, 31],
|
|
10
5
|
[1, 12],
|
|
11
|
-
[0, 7],
|
|
6
|
+
[0, 7],
|
|
12
7
|
];
|
|
13
8
|
function parseField(part, min, max) {
|
|
14
9
|
const out = new Set();
|
|
15
10
|
const clampOrThrow = (v) => {
|
|
16
|
-
// accepting an out-of-range value produces a set that never matches, i.e. a
|
|
17
|
-
// cron that silently never fires ("99 * * * *")
|
|
18
11
|
if (!Number.isInteger(v) || v < min || v > max)
|
|
19
12
|
throw new Error(`bad cron field: ${part} (${min}-${max} expected)`);
|
|
20
13
|
return v;
|
|
@@ -73,12 +66,10 @@ export function parseSchedule(spec) {
|
|
|
73
66
|
fields: parts.map((p, i) => parseField(p, BOUNDS[i][0], BOUNDS[i][1])),
|
|
74
67
|
};
|
|
75
68
|
}
|
|
76
|
-
/** Does this schedule fire at the given Date? */
|
|
77
69
|
export function matches(schedule, d) {
|
|
78
70
|
const values = [d.getMinutes(), d.getHours(), d.getDate(), d.getMonth() + 1, d.getDay()];
|
|
79
71
|
for (let i = 0; i < 5; i++) {
|
|
80
72
|
if (!schedule.fields[i].has(values[i])) {
|
|
81
|
-
// Sunday alias: field 5 with 7 should also match day=0
|
|
82
73
|
if (i === 4 && values[i] === 0 && schedule.fields[i].has(7))
|
|
83
74
|
continue;
|
|
84
75
|
return false;
|
|
@@ -86,14 +77,8 @@ export function matches(schedule, d) {
|
|
|
86
77
|
}
|
|
87
78
|
return true;
|
|
88
79
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Next fire time after `from`, scanning minute by minute (cheap: a handful of
|
|
91
|
-
* integer compares per minute). The horizon is a year so an annual cron (a
|
|
92
|
-
* December date seen in August) still resolves; a Feb-29-only cron can
|
|
93
|
-
* legitimately report null most of the year.
|
|
94
|
-
*/
|
|
95
80
|
export function nextFireAt(schedule, from = new Date()) {
|
|
96
|
-
const startMinute = Math.floor(from.getTime() / 60_000) + 1;
|
|
81
|
+
const startMinute = Math.floor(from.getTime() / 60_000) + 1;
|
|
97
82
|
const horizon = 60 * 24 * 366;
|
|
98
83
|
for (let i = 0; i < horizon; i++) {
|
|
99
84
|
const d = new Date((startMinute + i) * 60_000);
|