@foldrun/core 0.2.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/LICENSE +202 -0
- package/README.md +239 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +43 -0
- package/dist/src/agent-tools.d.ts +31 -0
- package/dist/src/agent-tools.js +99 -0
- package/dist/src/agents-md.d.ts +9 -0
- package/dist/src/agents-md.js +83 -0
- package/dist/src/api-tools.d.ts +38 -0
- package/dist/src/api-tools.js +313 -0
- package/dist/src/approvals.d.ts +39 -0
- package/dist/src/approvals.js +103 -0
- package/dist/src/arrange.d.ts +8 -0
- package/dist/src/arrange.js +40 -0
- package/dist/src/catalog.d.ts +108 -0
- package/dist/src/catalog.js +255 -0
- package/dist/src/completions.d.ts +61 -0
- package/dist/src/completions.js +578 -0
- package/dist/src/confine.d.ts +31 -0
- package/dist/src/confine.js +298 -0
- package/dist/src/container.d.ts +30 -0
- package/dist/src/container.js +172 -0
- package/dist/src/context-tools.d.ts +81 -0
- package/dist/src/context-tools.js +347 -0
- package/dist/src/deploy.d.ts +100 -0
- package/dist/src/deploy.js +377 -0
- package/dist/src/egress.d.ts +75 -0
- package/dist/src/egress.js +133 -0
- package/dist/src/evals.d.ts +150 -0
- package/dist/src/evals.js +651 -0
- package/dist/src/flow-lint.d.ts +16 -0
- package/dist/src/flow-lint.js +177 -0
- package/dist/src/git-hook.d.ts +1 -0
- package/dist/src/git-hook.js +70 -0
- package/dist/src/git.d.ts +37 -0
- package/dist/src/git.js +101 -0
- package/dist/src/gitrepo.d.ts +95 -0
- package/dist/src/gitrepo.js +355 -0
- package/dist/src/history.d.ts +82 -0
- package/dist/src/history.js +259 -0
- package/dist/src/kinds.d.ts +95 -0
- package/dist/src/kinds.js +421 -0
- package/dist/src/library.d.ts +94 -0
- package/dist/src/library.js +341 -0
- package/dist/src/linkable.d.ts +20 -0
- package/dist/src/linkable.js +113 -0
- package/dist/src/model-probe.d.ts +21 -0
- package/dist/src/model-probe.js +81 -0
- package/dist/src/notify.d.ts +61 -0
- package/dist/src/notify.js +252 -0
- package/dist/src/oauth-presets.d.ts +13 -0
- package/dist/src/oauth-presets.js +39 -0
- package/dist/src/okf.d.ts +220 -0
- package/dist/src/okf.js +656 -0
- package/dist/src/openapi.d.ts +66 -0
- package/dist/src/openapi.js +342 -0
- package/dist/src/paths.d.ts +33 -0
- package/dist/src/paths.js +78 -0
- package/dist/src/platform.d.ts +32 -0
- package/dist/src/platform.js +48 -0
- package/dist/src/providers.d.ts +47 -0
- package/dist/src/providers.js +114 -0
- package/dist/src/refs.d.ts +20 -0
- package/dist/src/refs.js +78 -0
- package/dist/src/run-container.d.ts +227 -0
- package/dist/src/run-container.js +760 -0
- package/dist/src/runner.d.ts +241 -0
- package/dist/src/runner.js +3215 -0
- package/dist/src/runtime.d.ts +41 -0
- package/dist/src/runtime.js +357 -0
- package/dist/src/script-tools.d.ts +62 -0
- package/dist/src/script-tools.js +296 -0
- package/dist/src/secret-files.d.ts +12 -0
- package/dist/src/secret-files.js +92 -0
- package/dist/src/secrets.d.ts +147 -0
- package/dist/src/secrets.js +626 -0
- package/dist/src/starter.d.ts +21 -0
- package/dist/src/starter.js +399 -0
- package/dist/src/step-exec.d.ts +136 -0
- package/dist/src/step-exec.js +391 -0
- package/dist/src/storage.d.ts +123 -0
- package/dist/src/storage.js +640 -0
- package/dist/src/store.d.ts +1052 -0
- package/dist/src/store.js +2378 -0
- package/dist/src/tar.d.ts +30 -0
- package/dist/src/tar.js +111 -0
- package/dist/src/tool-names.d.ts +39 -0
- package/dist/src/tool-names.js +79 -0
- package/dist/src/tool-programs.d.ts +18 -0
- package/dist/src/tool-programs.js +52 -0
- package/dist/src/tool-test.d.ts +19 -0
- package/dist/src/tool-test.js +335 -0
- package/dist/src/translator.d.ts +159 -0
- package/dist/src/translator.js +986 -0
- package/dist/src/triggers.d.ts +60 -0
- package/dist/src/triggers.js +270 -0
- package/dist/src/webhook.d.ts +47 -0
- package/dist/src/webhook.js +148 -0
- package/index.ts +44 -0
- package/package.json +64 -0
- package/src/agent-tools.ts +138 -0
- package/src/agents-md.ts +81 -0
- package/src/api-tools.ts +389 -0
- package/src/approvals.ts +129 -0
- package/src/arrange.ts +42 -0
- package/src/catalog.ts +321 -0
- package/src/completions.ts +652 -0
- package/src/confine.ts +350 -0
- package/src/container.ts +220 -0
- package/src/context-tools.ts +435 -0
- package/src/deploy.ts +458 -0
- package/src/egress.ts +176 -0
- package/src/evals.ts +812 -0
- package/src/flow-lint.ts +215 -0
- package/src/git-hook.ts +73 -0
- package/src/git.ts +117 -0
- package/src/gitrepo.ts +391 -0
- package/src/history.ts +329 -0
- package/src/kinds.ts +507 -0
- package/src/library.ts +419 -0
- package/src/linkable.ts +123 -0
- package/src/model-probe.ts +112 -0
- package/src/notify.ts +281 -0
- package/src/oauth-presets.ts +54 -0
- package/src/okf.ts +768 -0
- package/src/openapi.ts +383 -0
- package/src/paths.ts +79 -0
- package/src/platform.ts +87 -0
- package/src/providers.ts +147 -0
- package/src/refs.ts +83 -0
- package/src/run-container.ts +883 -0
- package/src/runner.ts +3446 -0
- package/src/runtime.ts +404 -0
- package/src/script-tools.ts +383 -0
- package/src/secret-files.ts +101 -0
- package/src/secrets.ts +799 -0
- package/src/starter.ts +415 -0
- package/src/step-exec.ts +498 -0
- package/src/storage.ts +771 -0
- package/src/store.ts +2985 -0
- package/src/tar.ts +124 -0
- package/src/tool-names.ts +91 -0
- package/src/tool-programs.ts +65 -0
- package/src/tool-test.ts +401 -0
- package/src/translator.ts +1029 -0
- package/src/triggers.ts +287 -0
- package/src/webhook.ts +183 -0
- package/templates/blog-desk/AGENTS.md +13 -0
- package/templates/blog-desk/agents/researcher/agent.md +11 -0
- package/templates/blog-desk/agents/writer/agent.md +8 -0
- package/templates/blog-desk/evals/writer-quality.md +10 -0
- package/templates/blog-desk/flows/publish.md +10 -0
- package/templates/blog-desk/knowledge/house-style.md +9 -0
- package/templates/blog-desk/knowledge/index.md +9 -0
- package/templates/hello/AGENTS.md +18 -0
- package/templates/hello/agents/curator/agent.md +32 -0
- package/templates/hello/agents/notetaker/agent.md +24 -0
- package/templates/hello/evals/house-style.md +16 -0
- package/templates/hello/flows/memory-review.md +7 -0
- package/templates/hello/flows/note.md +7 -0
- package/templates/hello/knowledge/house-style.md +15 -0
- package/templates/hello/knowledge/index.md +9 -0
- package/templates/hello/skills/plain-english/SKILL.md +11 -0
- package/templates/hello/state/publishing.md +7 -0
- package/templates/hello/tools/wordcount/run.py +13 -0
- package/templates/hello/tools/wordcount/tool.md +22 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type Effort, type Tier, type FlowInfo } from "./store.ts";
|
|
2
|
+
import type { FlowWarning } from "./flow-lint.ts";
|
|
3
|
+
export interface CatalogModel {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
contextLength: number | null;
|
|
7
|
+
/** USD per input token, as the gateway quotes it (not per MTok). */
|
|
8
|
+
promptPrice: number | null;
|
|
9
|
+
/** USD per output token. */
|
|
10
|
+
completionPrice: number | null;
|
|
11
|
+
/** Can this model make tool calls? Null when the gateway doesn't say. */
|
|
12
|
+
tools: boolean | null;
|
|
13
|
+
/** Does it take a reasoning-depth parameter? (What `effort:` needs.) */
|
|
14
|
+
reasoning: boolean | null;
|
|
15
|
+
/** The effort levels this model accepts, when the gateway names them —
|
|
16
|
+
* OpenRouter's level names are ours (low…max, plus minimal/none below
|
|
17
|
+
* our scale), so no translation table sits between the two. */
|
|
18
|
+
efforts: string[] | null;
|
|
19
|
+
}
|
|
20
|
+
export interface Catalog {
|
|
21
|
+
/** The base_url this catalogue was fetched from. */
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
fetchedAt: string;
|
|
24
|
+
models: CatalogModel[];
|
|
25
|
+
}
|
|
26
|
+
/** The cache as it stands, however old — null only when nothing was ever
|
|
27
|
+
* fetched. Reading never touches the network. */
|
|
28
|
+
export declare function readCatalog(baseUrl: string): Catalog | null;
|
|
29
|
+
/**
|
|
30
|
+
* The cache, refreshed over the network when it is older than the TTL.
|
|
31
|
+
* A failed fetch keeps the stale copy — offline is a reason to be out of
|
|
32
|
+
* date, not a reason to forget everything — and returns null only when
|
|
33
|
+
* there is nothing at all to fall back on.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadCatalog(baseUrl: string): Promise<Catalog | null>;
|
|
36
|
+
/** A preset reference — settings live in the gateway's dashboard, opaque to
|
|
37
|
+
* us on purpose. The catalogue offers no opinion on one. */
|
|
38
|
+
export declare const isPresetRef: (model: string) => boolean;
|
|
39
|
+
export declare function findModel(catalog: Catalog | null, id: string): CatalogModel | null;
|
|
40
|
+
export interface ModelVerdict {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
/** One line, naming the cause and the nearest fix. Only set when !ok. */
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The run-start gate. Only ever fails on *knowledge*: an id the catalogue
|
|
47
|
+
* has, whose declared capabilities rule the step out. Unknown ids, preset
|
|
48
|
+
* refs, tier names that resolved elsewhere, and an absent catalogue all
|
|
49
|
+
* pass — the model call itself is the authority of last resort, and a gate
|
|
50
|
+
* that blocks on ignorance would make offline development impossible.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkModel(catalog: Catalog | null, modelId: string, needs: {
|
|
53
|
+
tools: boolean;
|
|
54
|
+
}): ModelVerdict;
|
|
55
|
+
/** True when `effort:` will be silently ignored by this model — worth a
|
|
56
|
+
* trace line, never a failure: the step still runs, just without the depth
|
|
57
|
+
* the file asked for. */
|
|
58
|
+
export declare function effortIgnored(catalog: Catalog | null, modelId: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The effort that will actually reach this model, and the sentence that
|
|
61
|
+
* explains any difference. Three outcomes:
|
|
62
|
+
*
|
|
63
|
+
* - the model takes this level → unchanged, silent
|
|
64
|
+
* - it reasons, but not at this level → nearest supported level, said aloud
|
|
65
|
+
* - it does not reason at all → null, said aloud
|
|
66
|
+
*
|
|
67
|
+
* "Nearest" prefers the highest supported level at or below the request —
|
|
68
|
+
* someone who wrote `max` wanted the ceiling, and the ceiling here is
|
|
69
|
+
* `xhigh` — falling back to the lowest above it. Unknown models and absent
|
|
70
|
+
* catalogues pass the request through untouched: no knowledge, no opinion,
|
|
71
|
+
* same as the gate.
|
|
72
|
+
*/
|
|
73
|
+
export declare function clampEffort(catalog: Catalog | null, modelId: string, effort: Effort | null): {
|
|
74
|
+
effort: Effort | null;
|
|
75
|
+
note: string | null;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* What one step actually cost through this gateway, or null when the
|
|
79
|
+
* catalogue can't say. The SDK's own costUsd is priced off Anthropic's
|
|
80
|
+
* table, which is wrong for every routed model — so when the catalogue
|
|
81
|
+
* knows better, its number wins.
|
|
82
|
+
*/
|
|
83
|
+
export declare function catalogCost(catalog: Catalog | null, modelId: string, usage: {
|
|
84
|
+
inputTokens: number;
|
|
85
|
+
outputTokens: number;
|
|
86
|
+
}): number | null;
|
|
87
|
+
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
88
|
+
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
89
|
+
* on an agent platform they are almost always what the author wants. */
|
|
90
|
+
export declare function catalogCompletions(catalog: Catalog | null): {
|
|
91
|
+
id: string;
|
|
92
|
+
hint: string;
|
|
93
|
+
}[];
|
|
94
|
+
/** What the lint needs to know about each agent: its own model, and whether
|
|
95
|
+
* it declares any tools at all. */
|
|
96
|
+
export interface AgentModelInfo {
|
|
97
|
+
name: string;
|
|
98
|
+
model: string;
|
|
99
|
+
usesTools: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The run-start gate's veto, raised while the file is still open. Same
|
|
103
|
+
* knowledge, earlier moment: a step whose resolved model the catalogue says
|
|
104
|
+
* cannot call tools — run by an agent that declares some — gets a warning in
|
|
105
|
+
* the editor instead of a failed step tomorrow morning. Advisory like every
|
|
106
|
+
* flow lint; the run-start gate stays the enforcement.
|
|
107
|
+
*/
|
|
108
|
+
export declare function lintFlowModels(flow: FlowInfo, agents: AgentModelInfo[], catalog: Catalog | null, providerModels?: Partial<Record<Tier, string>>): FlowWarning[];
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// The model catalogue: what a gateway can actually run, cached.
|
|
2
|
+
//
|
|
3
|
+
// "Any model they want" is only an offer if picking a wrong one fails in one
|
|
4
|
+
// line at step start — not three minutes in, as an agent that narrates tool
|
|
5
|
+
// calls into prose because its model cannot make them. OpenRouter's models
|
|
6
|
+
// endpoint declares, per model, which parameters it supports (`tools` being
|
|
7
|
+
// the one that decides whether an agent loop is possible at all), plus
|
|
8
|
+
// context length and real prices. This module fetches that once, caches it
|
|
9
|
+
// on disk, and answers three questions from the cache:
|
|
10
|
+
//
|
|
11
|
+
// - can this model drive tools? → the run-start gate
|
|
12
|
+
// - what models exist, at what price? → `model:` autocomplete
|
|
13
|
+
// - what does a token actually cost? → metering for routed models
|
|
14
|
+
//
|
|
15
|
+
// Everything degrades to "don't know": a missing cache, an offline fetch or
|
|
16
|
+
// an unknown id yields null, and callers treat null as "no opinion" — the
|
|
17
|
+
// gate never blocks on ignorance, only on knowledge. A preset reference
|
|
18
|
+
// (`@preset/…`) is opaque by design: its settings live in the provider's
|
|
19
|
+
// dashboard, so the catalogue has no opinion about it either.
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { dataRoot } from "./paths.js";
|
|
23
|
+
import { resolveModel, resolveTier, MODEL_TIERS, EFFORT_LEVELS } from "./store.js";
|
|
24
|
+
/** A day. Model lists move weekly, not hourly, and a stale price in an
|
|
25
|
+
* autocomplete hint is a smaller failure than a fetch on every keystroke. */
|
|
26
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
27
|
+
const cacheFile = (baseUrl) => path.join(dataRoot(), "catalog",
|
|
28
|
+
// One file per endpoint, named legibly: catalog/openrouter.ai.json.
|
|
29
|
+
`${new URL(baseUrl).hostname.replace(/[^a-z0-9.-]/gi, "_")}.json`);
|
|
30
|
+
function parseModels(payload) {
|
|
31
|
+
const data = payload?.data;
|
|
32
|
+
if (!Array.isArray(data))
|
|
33
|
+
return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const raw of data) {
|
|
36
|
+
if (!raw || typeof raw !== "object")
|
|
37
|
+
continue;
|
|
38
|
+
const m = raw;
|
|
39
|
+
if (typeof m.id !== "string")
|
|
40
|
+
continue;
|
|
41
|
+
const supported = Array.isArray(m.supported_parameters)
|
|
42
|
+
? m.supported_parameters.map(String)
|
|
43
|
+
: null;
|
|
44
|
+
const pricing = (m.pricing ?? {});
|
|
45
|
+
const price = (k) => {
|
|
46
|
+
const n = Number(pricing[k]);
|
|
47
|
+
return Number.isFinite(n) ? n : null;
|
|
48
|
+
};
|
|
49
|
+
out.push({
|
|
50
|
+
id: m.id,
|
|
51
|
+
name: typeof m.name === "string" ? m.name : m.id,
|
|
52
|
+
contextLength: Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : null,
|
|
53
|
+
promptPrice: price("prompt"),
|
|
54
|
+
completionPrice: price("completion"),
|
|
55
|
+
tools: supported ? supported.includes("tools") : null,
|
|
56
|
+
reasoning: supported ? supported.includes("reasoning") : null,
|
|
57
|
+
efforts: Array.isArray(m.reasoning?.supported_efforts)
|
|
58
|
+
? m.reasoning.supported_efforts.map(String)
|
|
59
|
+
: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** The cache as it stands, however old — null only when nothing was ever
|
|
65
|
+
* fetched. Reading never touches the network. */
|
|
66
|
+
export function readCatalog(baseUrl) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFile(baseUrl), "utf8"));
|
|
69
|
+
return Array.isArray(parsed.models) ? parsed : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The cache, refreshed over the network when it is older than the TTL.
|
|
77
|
+
* A failed fetch keeps the stale copy — offline is a reason to be out of
|
|
78
|
+
* date, not a reason to forget everything — and returns null only when
|
|
79
|
+
* there is nothing at all to fall back on.
|
|
80
|
+
*/
|
|
81
|
+
export async function loadCatalog(baseUrl) {
|
|
82
|
+
const cached = readCatalog(baseUrl);
|
|
83
|
+
if (cached && Date.now() - Date.parse(cached.fetchedAt) < TTL_MS)
|
|
84
|
+
return cached;
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/models`, {
|
|
87
|
+
signal: AbortSignal.timeout(10_000),
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok)
|
|
90
|
+
throw new Error(`${res.status}`);
|
|
91
|
+
const models = parseModels(await res.json());
|
|
92
|
+
if (!models.length)
|
|
93
|
+
throw new Error("empty model list");
|
|
94
|
+
const catalog = { baseUrl, fetchedAt: new Date().toISOString(), models };
|
|
95
|
+
fs.mkdirSync(path.dirname(cacheFile(baseUrl)), { recursive: true });
|
|
96
|
+
// Atomic like every other store write: a crashed refresh must not leave
|
|
97
|
+
// half a JSON file where the whole catalogue used to be.
|
|
98
|
+
const tmp = cacheFile(baseUrl) + ".tmp";
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(catalog));
|
|
100
|
+
fs.renameSync(tmp, cacheFile(baseUrl));
|
|
101
|
+
return catalog;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return cached; // stale beats nothing; null when there is no nothing
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** A preset reference — settings live in the gateway's dashboard, opaque to
|
|
108
|
+
* us on purpose. The catalogue offers no opinion on one. */
|
|
109
|
+
export const isPresetRef = (model) => model.startsWith("@preset/");
|
|
110
|
+
export function findModel(catalog, id) {
|
|
111
|
+
if (!catalog || isPresetRef(id))
|
|
112
|
+
return null;
|
|
113
|
+
return catalog.models.find((m) => m.id === id) ?? null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The run-start gate. Only ever fails on *knowledge*: an id the catalogue
|
|
117
|
+
* has, whose declared capabilities rule the step out. Unknown ids, preset
|
|
118
|
+
* refs, tier names that resolved elsewhere, and an absent catalogue all
|
|
119
|
+
* pass — the model call itself is the authority of last resort, and a gate
|
|
120
|
+
* that blocks on ignorance would make offline development impossible.
|
|
121
|
+
*/
|
|
122
|
+
export function checkModel(catalog, modelId, needs) {
|
|
123
|
+
const m = findModel(catalog, modelId);
|
|
124
|
+
if (!m)
|
|
125
|
+
return { ok: true };
|
|
126
|
+
if (needs.tools && m.tools === false) {
|
|
127
|
+
const alternative = nearestWithTools(catalog, m);
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
reason: `model ${m.id} cannot call tools, and this agent declares some — the run would ` +
|
|
131
|
+
`narrate tool use as text instead of doing it.` +
|
|
132
|
+
(alternative ? ` Nearest model that can: ${alternative.id}.` : ""),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
/** Same family first (everything before the slash), then anything with
|
|
138
|
+
* tools — "use the provider's sibling" is advice people actually take. */
|
|
139
|
+
function nearestWithTools(catalog, from) {
|
|
140
|
+
const vendor = from.id.split("/")[0];
|
|
141
|
+
const candidates = catalog.models.filter((m) => m.tools === true);
|
|
142
|
+
return candidates.find((m) => m.id.startsWith(vendor + "/")) ?? candidates[0] ?? null;
|
|
143
|
+
}
|
|
144
|
+
/** True when `effort:` will be silently ignored by this model — worth a
|
|
145
|
+
* trace line, never a failure: the step still runs, just without the depth
|
|
146
|
+
* the file asked for. */
|
|
147
|
+
export function effortIgnored(catalog, modelId) {
|
|
148
|
+
const m = findModel(catalog, modelId);
|
|
149
|
+
return m !== null && m.reasoning === false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The effort that will actually reach this model, and the sentence that
|
|
153
|
+
* explains any difference. Three outcomes:
|
|
154
|
+
*
|
|
155
|
+
* - the model takes this level → unchanged, silent
|
|
156
|
+
* - it reasons, but not at this level → nearest supported level, said aloud
|
|
157
|
+
* - it does not reason at all → null, said aloud
|
|
158
|
+
*
|
|
159
|
+
* "Nearest" prefers the highest supported level at or below the request —
|
|
160
|
+
* someone who wrote `max` wanted the ceiling, and the ceiling here is
|
|
161
|
+
* `xhigh` — falling back to the lowest above it. Unknown models and absent
|
|
162
|
+
* catalogues pass the request through untouched: no knowledge, no opinion,
|
|
163
|
+
* same as the gate.
|
|
164
|
+
*/
|
|
165
|
+
export function clampEffort(catalog, modelId, effort) {
|
|
166
|
+
if (!effort)
|
|
167
|
+
return { effort: null, note: null };
|
|
168
|
+
const m = findModel(catalog, modelId);
|
|
169
|
+
if (!m)
|
|
170
|
+
return { effort, note: null };
|
|
171
|
+
if (m.reasoning === false && !m.efforts?.length) {
|
|
172
|
+
return {
|
|
173
|
+
effort: null,
|
|
174
|
+
note: `model ${m.id} takes no reasoning parameter — effort: ${effort} will have no effect`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (!m.efforts?.length)
|
|
178
|
+
return { effort, note: null }; // reasons, levels unnamed
|
|
179
|
+
const supported = EFFORT_LEVELS.filter((l) => m.efforts.includes(l));
|
|
180
|
+
if (!supported.length || supported.includes(effort))
|
|
181
|
+
return { effort, note: null };
|
|
182
|
+
const want = EFFORT_LEVELS.indexOf(effort);
|
|
183
|
+
const below = [...supported].reverse().find((l) => EFFORT_LEVELS.indexOf(l) < want);
|
|
184
|
+
const clamped = below ?? supported[0];
|
|
185
|
+
return {
|
|
186
|
+
effort: clamped,
|
|
187
|
+
note: `model ${m.id} supports effort ${supported.join(", ")} — ` +
|
|
188
|
+
`effort: ${effort} runs as ${clamped}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* What one step actually cost through this gateway, or null when the
|
|
193
|
+
* catalogue can't say. The SDK's own costUsd is priced off Anthropic's
|
|
194
|
+
* table, which is wrong for every routed model — so when the catalogue
|
|
195
|
+
* knows better, its number wins.
|
|
196
|
+
*/
|
|
197
|
+
export function catalogCost(catalog, modelId, usage) {
|
|
198
|
+
const m = findModel(catalog, modelId);
|
|
199
|
+
if (!m || m.promptPrice == null || m.completionPrice == null)
|
|
200
|
+
return null;
|
|
201
|
+
return usage.inputTokens * m.promptPrice + usage.outputTokens * m.completionPrice;
|
|
202
|
+
}
|
|
203
|
+
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
204
|
+
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
205
|
+
* on an agent platform they are almost always what the author wants. */
|
|
206
|
+
export function catalogCompletions(catalog) {
|
|
207
|
+
if (!catalog)
|
|
208
|
+
return [];
|
|
209
|
+
const perMTok = (v) => (v == null ? "?" : `$${(v * 1_000_000).toFixed(2)}`);
|
|
210
|
+
const ctx = (n) => (n == null ? "" : n >= 1000 ? ` · ${Math.round(n / 1000)}k ctx` : ` · ${n} ctx`);
|
|
211
|
+
return [...catalog.models]
|
|
212
|
+
.sort((a, b) => Number(b.tools === true) - Number(a.tools === true) || a.id.localeCompare(b.id))
|
|
213
|
+
.map((m) => ({
|
|
214
|
+
id: m.id,
|
|
215
|
+
hint: `${perMTok(m.promptPrice)}/${perMTok(m.completionPrice)} MTok${ctx(m.contextLength)}${m.tools === false ? " · no tools" : ""}`,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The run-start gate's veto, raised while the file is still open. Same
|
|
220
|
+
* knowledge, earlier moment: a step whose resolved model the catalogue says
|
|
221
|
+
* cannot call tools — run by an agent that declares some — gets a warning in
|
|
222
|
+
* the editor instead of a failed step tomorrow morning. Advisory like every
|
|
223
|
+
* flow lint; the run-start gate stays the enforcement.
|
|
224
|
+
*/
|
|
225
|
+
export function lintFlowModels(flow, agents, catalog, providerModels = {}) {
|
|
226
|
+
if (!catalog)
|
|
227
|
+
return [];
|
|
228
|
+
const warnings = [];
|
|
229
|
+
const byName = new Map(agents.map((a) => [a.name, a]));
|
|
230
|
+
// The same remap the runner applies on the wire: a tier the provider
|
|
231
|
+
// block renames is checked as the gateway's id.
|
|
232
|
+
const wire = (value) => {
|
|
233
|
+
const resolved = resolveModel(value);
|
|
234
|
+
const tier = Object.entries(MODEL_TIERS).find(([, id]) => id === resolved)?.[0] ?? resolveTier(value);
|
|
235
|
+
return (tier && providerModels[tier]) || resolved;
|
|
236
|
+
};
|
|
237
|
+
flow.steps.forEach((step, i) => {
|
|
238
|
+
if (step.subflow)
|
|
239
|
+
return; // the subflow's own lint covers its steps
|
|
240
|
+
const agent = byName.get(step.agent);
|
|
241
|
+
if (!agent?.usesTools)
|
|
242
|
+
return;
|
|
243
|
+
const model = wire(step.model ?? flow.model ?? agent.model);
|
|
244
|
+
const verdict = checkModel(catalog, model, { tools: true });
|
|
245
|
+
if (!verdict.ok) {
|
|
246
|
+
warnings.push({
|
|
247
|
+
step: i,
|
|
248
|
+
line: step.line,
|
|
249
|
+
message: `"${step.agent}" would run on ${model}, which cannot call tools`,
|
|
250
|
+
detail: verdict.reason,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return warnings;
|
|
255
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface Vocabulary {
|
|
2
|
+
agents: string[];
|
|
3
|
+
flows: string[];
|
|
4
|
+
skills: string[];
|
|
5
|
+
tools: string[];
|
|
6
|
+
secrets: string[];
|
|
7
|
+
scripts: string[];
|
|
8
|
+
/** Everything [[ ]] can point at besides agents and flows: knowledge and
|
|
9
|
+
* memory docs, and paths in the file store. One reference syntax for the
|
|
10
|
+
* whole workspace — the Obsidian instinct, honoured. */
|
|
11
|
+
docs?: {
|
|
12
|
+
name: string;
|
|
13
|
+
hint: string;
|
|
14
|
+
}[];
|
|
15
|
+
/** OKF `type:` values already used here. The index groups by exact string,
|
|
16
|
+
* so offering what exists is what keeps a bundle consistent. */
|
|
17
|
+
types: string[];
|
|
18
|
+
/** The gateway's catalogue, when this workspace declares a provider —
|
|
19
|
+
* full ids with price and capability in the hint. Tiers stay first:
|
|
20
|
+
* they are the portable answer, the catalogue is the informed one. */
|
|
21
|
+
models?: {
|
|
22
|
+
id: string;
|
|
23
|
+
hint: string;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
export interface Completion {
|
|
27
|
+
label: string;
|
|
28
|
+
/** Text to insert in place of the query. Defaults to label. */
|
|
29
|
+
insert?: string;
|
|
30
|
+
hint?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CompletionContext {
|
|
33
|
+
/** Character offset where the replacement starts. */
|
|
34
|
+
from: number;
|
|
35
|
+
query: string;
|
|
36
|
+
items: Completion[];
|
|
37
|
+
/** Shown above the list, e.g. "HTTP method". */
|
|
38
|
+
title: string;
|
|
39
|
+
}
|
|
40
|
+
declare const FIELDS: Record<string, Completion[]>;
|
|
41
|
+
export type FileKind = keyof typeof FIELDS;
|
|
42
|
+
export declare function fileKind(path: string): FileKind;
|
|
43
|
+
/**
|
|
44
|
+
* Does [[ ]] in this file get expanded into a path, or stay literal?
|
|
45
|
+
*
|
|
46
|
+
* The rule is not "which directory" but "does this text become a prompt".
|
|
47
|
+
* An agent's body, a flow step's instruction and an eval's task all end up
|
|
48
|
+
* in front of a model, which cannot open `[[house-style]]` but can open
|
|
49
|
+
* `../../knowledge/house-style.md`. A knowledge or tool document is read as
|
|
50
|
+
* a file, and its links are for the person reading it.
|
|
51
|
+
*
|
|
52
|
+
* Exported so the editor can say which case a file is in, rather than
|
|
53
|
+
* offering the same word for two different behaviours.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolvesLinks(path: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* What can go at the cursor?
|
|
58
|
+
* @returns null when nothing sensible can be suggested.
|
|
59
|
+
*/
|
|
60
|
+
export declare function completionsAt(path: string, text: string, cursor: number, vocab: Vocabulary): CompletionContext | null;
|
|
61
|
+
export {};
|