mindweave 2.4.7 → 2.4.9
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 +5 -3
- package/dist/cli/App.js +70 -23
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/chatAnchor.js +22 -0
- package/dist/cli/chatAnchor.js.map +1 -1
- package/dist/cli/commandArgs.js +60 -16
- package/dist/cli/commandArgs.js.map +1 -1
- package/dist/cli/components/Picker.js +61 -19
- package/dist/cli/components/Picker.js.map +1 -1
- package/dist/cli/scrollPill.js +4 -4
- package/dist/cli/scrollPill.js.map +1 -1
- package/dist/drivers/clientId.js +2 -3
- package/dist/drivers/clientId.js.map +1 -1
- package/dist/drivers/openaiCompat/wire.js +55 -3
- package/dist/drivers/openaiCompat/wire.js.map +1 -1
- package/dist/drivers/openrouter/catalog.js +136 -0
- package/dist/drivers/openrouter/catalog.js.map +1 -0
- package/dist/drivers/openrouter/client.js +102 -0
- package/dist/drivers/openrouter/client.js.map +1 -0
- package/dist/drivers/openrouter/endpoint.js +20 -0
- package/dist/drivers/openrouter/endpoint.js.map +1 -0
- package/dist/drivers/openrouter/index.js +5 -0
- package/dist/drivers/openrouter/index.js.map +1 -0
- package/dist/drivers/openrouter/manifest.js +83 -0
- package/dist/drivers/openrouter/manifest.js.map +1 -0
- package/dist/drivers/providerError.js +51 -0
- package/dist/drivers/providerError.js.map +1 -1
- package/dist/drivers/registry.js +111 -3
- package/dist/drivers/registry.js.map +1 -1
- package/dist/dynamo/model.js +63 -4
- package/dist/dynamo/model.js.map +1 -1
- package/package.json +74 -74
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { BASE_URL, OPENROUTER_HEADERS } from "./endpoint.js";
|
|
2
|
+
import { PREFIX, USABLE_WINDOW_CAP } from "./manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a model can serve an agent turn here at all.
|
|
5
|
+
*
|
|
6
|
+
* Everything that works is listed; a model is left out only when it would fail on
|
|
7
|
+
* first use or cannot be priced:
|
|
8
|
+
* - no text output, or no `tools` — an agent turn needs both;
|
|
9
|
+
* - `:batch` entries, which only answer through the Batch API;
|
|
10
|
+
* - OpenRouter's own routers (`openrouter/…`, price `-1`), whose model, price and
|
|
11
|
+
* window are unknown until after the call;
|
|
12
|
+
* - `~` aliases, which point at whatever is newest, so a saved choice would silently
|
|
13
|
+
* change model, price and behaviour.
|
|
14
|
+
*/
|
|
15
|
+
export function isUsable(entry) {
|
|
16
|
+
if (!entry.id || entry.id.startsWith("~") || entry.id.startsWith("openrouter/"))
|
|
17
|
+
return false;
|
|
18
|
+
if (entry.id.endsWith(":batch"))
|
|
19
|
+
return false;
|
|
20
|
+
if (!(entry.architecture?.output_modalities ?? []).includes("text"))
|
|
21
|
+
return false;
|
|
22
|
+
if (!(entry.supported_parameters ?? []).includes("tools"))
|
|
23
|
+
return false;
|
|
24
|
+
const prompt = Number(entry.pricing?.prompt);
|
|
25
|
+
return Number.isFinite(prompt) && prompt >= 0;
|
|
26
|
+
}
|
|
27
|
+
const EFFORT_LEVELS = [
|
|
28
|
+
{ label: "Low", description: "reasoning effort: low", thinking: true, effort: "low" },
|
|
29
|
+
{ label: "Medium", description: "reasoning effort: medium", thinking: true, effort: "medium" },
|
|
30
|
+
{ label: "High", description: "reasoning effort: high", thinking: true, effort: "high" },
|
|
31
|
+
{ label: "Extra high", description: "reasoning effort: extra high", thinking: true, effort: "xhigh" },
|
|
32
|
+
{ label: "Maximum", description: "reasoning effort: maximum", thinking: true, effort: "max" },
|
|
33
|
+
];
|
|
34
|
+
const STANDARD = { label: "Standard", description: "answer directly — fastest", thinking: false, effort: "low" };
|
|
35
|
+
/**
|
|
36
|
+
* The `/think` ladder the catalogue says this model accepts.
|
|
37
|
+
*
|
|
38
|
+
* No reasoning object: one row, nothing to choose. A reasoning model whose thinking is
|
|
39
|
+
* `mandatory` gets no Standard row, because OpenRouter rejects turning it off. Effort
|
|
40
|
+
* rungs are the ones it lists that Mindweave also has (`minimal` has no equivalent). A
|
|
41
|
+
* model that only toggles reasoning, with no efforts, gets a single Thinking row.
|
|
42
|
+
*/
|
|
43
|
+
export function levelsFor(entry) {
|
|
44
|
+
const r = entry.reasoning;
|
|
45
|
+
if (!r)
|
|
46
|
+
return [{ ...STANDARD, description: "this model has no reasoning dial" }];
|
|
47
|
+
const offered = new Set(r.supported_efforts ?? []);
|
|
48
|
+
const rungs = EFFORT_LEVELS.filter((l) => offered.has(l.effort));
|
|
49
|
+
const on = rungs.length > 0 ? rungs : [{ label: "Thinking", description: "think first, then answer", thinking: true, effort: "high" }];
|
|
50
|
+
return r.mandatory ? on : [STANDARD, ...on];
|
|
51
|
+
}
|
|
52
|
+
/** Per-token price strings to USD per million. */
|
|
53
|
+
function perMillion(value) {
|
|
54
|
+
const n = Number(value);
|
|
55
|
+
return typeof value === "string" && value !== "" && Number.isFinite(n) && n >= 0 ? n * 1_000_000 : undefined;
|
|
56
|
+
}
|
|
57
|
+
export function priceFor(entry) {
|
|
58
|
+
const p = entry.pricing ?? {};
|
|
59
|
+
const miss = perMillion(p.prompt);
|
|
60
|
+
const output = perMillion(p.completion);
|
|
61
|
+
if (miss === undefined || output === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
const hit = perMillion(p.input_cache_read);
|
|
64
|
+
const write = perMillion(p.input_cache_write);
|
|
65
|
+
return { cacheMiss: miss, output, cacheHit: hit ?? miss, ...(write !== undefined ? { cacheWrite: write } : {}) };
|
|
66
|
+
}
|
|
67
|
+
/** "Anthropic: Claude Fable 5.1" → vendor "Anthropic", label "Claude Fable 5.1". */
|
|
68
|
+
function splitName(entry) {
|
|
69
|
+
const name = entry.name?.trim() || entry.id;
|
|
70
|
+
const colon = name.indexOf(": ");
|
|
71
|
+
if (colon > 0)
|
|
72
|
+
return { vendor: name.slice(0, colon), label: name.slice(colon + 2) };
|
|
73
|
+
return { vendor: entry.id.split("/")[0] ?? "", label: name };
|
|
74
|
+
}
|
|
75
|
+
function money(n) {
|
|
76
|
+
if (n === 0)
|
|
77
|
+
return "0";
|
|
78
|
+
return n >= 1 ? n.toFixed(2).replace(/\.00$/, "") : n.toPrecision(2).replace(/0+$/, "").replace(/\.$/, "");
|
|
79
|
+
}
|
|
80
|
+
export function toChoice(entry) {
|
|
81
|
+
const { vendor, label } = splitName(entry);
|
|
82
|
+
const price = priceFor(entry);
|
|
83
|
+
// `:free` variants share a tight daily request limit. A model can also simply cost
|
|
84
|
+
// nothing without the suffix (a stealth preview), and "$0 in / $0 out" reads as a
|
|
85
|
+
// missing price rather than a free one.
|
|
86
|
+
const limited = entry.id.endsWith(":free");
|
|
87
|
+
const free = limited || (price !== undefined && price.cacheMiss === 0 && price.output === 0);
|
|
88
|
+
const advertised = entry.top_provider?.context_length || entry.context_length || 0;
|
|
89
|
+
const facts = {
|
|
90
|
+
thinkLevels: levelsFor(entry),
|
|
91
|
+
acceptsImages: (entry.architecture?.input_modalities ?? []).includes("image"),
|
|
92
|
+
...(price ? { price } : {}),
|
|
93
|
+
...(advertised > 0 ? { contextWindow: Math.min(advertised, USABLE_WINDOW_CAP) } : {}),
|
|
94
|
+
};
|
|
95
|
+
const parts = [vendor];
|
|
96
|
+
if (free)
|
|
97
|
+
parts.push(limited ? "free, rate-limited" : "free");
|
|
98
|
+
else if (price)
|
|
99
|
+
parts.push(`$${money(price.cacheMiss)} in / $${money(price.output)} out per M`);
|
|
100
|
+
// Some hosts bill more at peak hours. The listed price is the base one, and saying so
|
|
101
|
+
// is better than a cost figure that quietly reads low half the day.
|
|
102
|
+
if (Array.isArray(entry.pricing?.overrides) && entry.pricing.overrides.length > 0)
|
|
103
|
+
parts.push("peak hours cost more");
|
|
104
|
+
const until = entry.expiration_date ? Date.parse(entry.expiration_date) : NaN;
|
|
105
|
+
return {
|
|
106
|
+
id: PREFIX + entry.id,
|
|
107
|
+
label,
|
|
108
|
+
description: parts.filter(Boolean).join(" · "),
|
|
109
|
+
facts,
|
|
110
|
+
...(Number.isFinite(until) ? { until } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function toChoices(entries) {
|
|
114
|
+
return entries.filter(isUsable).map(toChoice);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fetch the catalogue. It is public, but asked for only with a key: without one the
|
|
118
|
+
* provider cannot be used, and every launch would otherwise download it for nothing.
|
|
119
|
+
* Throws on any failure, so the registry keeps the list it had.
|
|
120
|
+
*/
|
|
121
|
+
export async function discoverModels() {
|
|
122
|
+
const key = process.env.OPENROUTER_API_KEY;
|
|
123
|
+
if (!key)
|
|
124
|
+
throw new Error("No OPENROUTER_API_KEY");
|
|
125
|
+
const response = await fetch(`${BASE_URL}/models`, {
|
|
126
|
+
headers: { Authorization: `Bearer ${key}`, ...OPENROUTER_HEADERS },
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok)
|
|
129
|
+
throw new Error(`OpenRouter catalogue: HTTP ${response.status}`);
|
|
130
|
+
const body = (await response.json());
|
|
131
|
+
const choices = toChoices(body.data ?? []);
|
|
132
|
+
if (choices.length === 0)
|
|
133
|
+
throw new Error("OpenRouter catalogue listed no usable models");
|
|
134
|
+
return choices;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../src/drivers/openrouter/catalog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { compatStreamTurn, compatToolTurn } from "../openaiCompat/wire.js";
|
|
2
|
+
import { BASE_URL, OPENROUTER_HEADERS } from "./endpoint.js";
|
|
3
|
+
import { BUFFERED_OUTPUT_TOKENS, DEFAULT_MODEL, wireId } from "./manifest.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reasoning, in OpenRouter's normalized field.
|
|
6
|
+
*
|
|
7
|
+
* Thinking on sends the chosen effort, which OpenRouter maps onto each vendor's own
|
|
8
|
+
* control. Thinking off sends `enabled: false`. The ladder the user picked from came
|
|
9
|
+
* from the catalogue, so a model whose reasoning is mandatory never reaches the off
|
|
10
|
+
* branch, and a model with no reasoning at all only ever has the off row.
|
|
11
|
+
*/
|
|
12
|
+
export function reasoningFields(config) {
|
|
13
|
+
if (!config)
|
|
14
|
+
return {};
|
|
15
|
+
return config.thinking ? { reasoning: { effort: config.effort } } : { reasoning: { enabled: false } };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The cache split, including writes.
|
|
19
|
+
*
|
|
20
|
+
* `prompt_tokens` is the whole prompt; `cached_tokens` the part read from cache and
|
|
21
|
+
* `cache_write_tokens` the part written to it. Writes are reported so the ones billed
|
|
22
|
+
* at a premium (Anthropic's 1.25x) are priced as such.
|
|
23
|
+
*/
|
|
24
|
+
export function cacheSplit(usage) {
|
|
25
|
+
const details = usage.prompt_tokens_details;
|
|
26
|
+
const hit = typeof details?.cached_tokens === "number" ? details.cached_tokens : 0;
|
|
27
|
+
const write = typeof details?.cache_write_tokens === "number" ? details.cache_write_tokens : 0;
|
|
28
|
+
if (hit === 0 && write === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const prompt = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0;
|
|
31
|
+
return { hit, miss: Math.max(0, prompt - hit), ...(write > 0 ? { write } : {}) };
|
|
32
|
+
}
|
|
33
|
+
/** Models that cache only what is marked. Every other family here caches automatically. */
|
|
34
|
+
function needsBreakpoints(model) {
|
|
35
|
+
return model.startsWith("anthropic/");
|
|
36
|
+
}
|
|
37
|
+
/** Put a breakpoint on a message's last content part, turning string content into parts. */
|
|
38
|
+
function mark(message) {
|
|
39
|
+
const content = message.content;
|
|
40
|
+
const breakpoint = { cache_control: { type: "ephemeral" } };
|
|
41
|
+
if (typeof content === "string") {
|
|
42
|
+
return { ...message, content: [{ type: "text", text: content, ...breakpoint }] };
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
45
|
+
const parts = [...content];
|
|
46
|
+
parts[parts.length - 1] = { ...parts[parts.length - 1], ...breakpoint };
|
|
47
|
+
return { ...message, content: parts };
|
|
48
|
+
}
|
|
49
|
+
return message;
|
|
50
|
+
}
|
|
51
|
+
function hasText(message) {
|
|
52
|
+
const content = message.content;
|
|
53
|
+
return (typeof content === "string" && content.length > 0) || (Array.isArray(content) && content.length > 0);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Explicit breakpoints at the two stable boundaries: the end of the system prompt, and
|
|
57
|
+
* the last message before the volatile context tail. The same boundaries the Anthropic
|
|
58
|
+
* driver uses, and never on the tail itself, which changes every step. An assistant
|
|
59
|
+
* message with only tool calls has no text to mark, so the boundary walks back to the
|
|
60
|
+
* nearest one that does.
|
|
61
|
+
*/
|
|
62
|
+
export function withBreakpoints(messages, hasContextTail) {
|
|
63
|
+
const out = [...messages];
|
|
64
|
+
if (out[0]?.role === "system" && hasText(out[0]))
|
|
65
|
+
out[0] = mark(out[0]);
|
|
66
|
+
let last = out.length - 1 - (hasContextTail ? 1 : 0);
|
|
67
|
+
while (last > 0 && !hasText(out[last]))
|
|
68
|
+
last--;
|
|
69
|
+
if (last > 0)
|
|
70
|
+
out[last] = mark(out[last]);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function finishBody(body, req) {
|
|
74
|
+
const model = wireId(String(body.model ?? DEFAULT_MODEL));
|
|
75
|
+
const out = { ...body, model };
|
|
76
|
+
if (needsBreakpoints(model) && Array.isArray(body.messages)) {
|
|
77
|
+
out.messages = withBreakpoints(body.messages, !!req.context?.trim());
|
|
78
|
+
}
|
|
79
|
+
// Allowed by default, which is OpenRouter's default too; the provider notice says so.
|
|
80
|
+
if (process.env.MINDWEAVE_OPENROUTER_DATA?.trim().toLowerCase() === "deny") {
|
|
81
|
+
out.provider = { data_collection: "deny" };
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export const openrouterProvider = {
|
|
86
|
+
label: "OpenRouter",
|
|
87
|
+
baseUrl: BASE_URL,
|
|
88
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
89
|
+
defaultModel: DEFAULT_MODEL,
|
|
90
|
+
reasoningFields,
|
|
91
|
+
cacheSplit,
|
|
92
|
+
bufferedMaxTokens: BUFFERED_OUTPUT_TOKENS,
|
|
93
|
+
headers: OPENROUTER_HEADERS,
|
|
94
|
+
finishBody,
|
|
95
|
+
};
|
|
96
|
+
export async function toolTurn(req, options = {}) {
|
|
97
|
+
return compatToolTurn(openrouterProvider, req, options.signal);
|
|
98
|
+
}
|
|
99
|
+
export async function streamTurn(req, options = {}) {
|
|
100
|
+
return compatStreamTurn(openrouterProvider, req, options.onEvent, options.signal);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/drivers/openrouter/client.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAuB,MAAM,yBAAyB,CAAC;AAChG,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAA+B;IAC7D,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC;AACxG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAA8B;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,qBAA8F,CAAC;IACrH,MAAM,GAAG,GAAG,OAAO,OAAO,EAAE,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,MAAM,KAAK,GAAG,OAAO,OAAO,EAAE,kBAAkB,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/C,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjF,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACnF,CAAC;AAED,2FAA2F;AAC3F,SAAS,gBAAgB,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACxC,CAAC;AAED,4FAA4F;AAC5F,SAAS,IAAI,CAAC,OAAgC;IAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,MAAM,UAAU,GAAG,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;IAC5D,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,EAAE,CAAC,EAAE,CAAC;IACnF,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;QAC3B,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAA6B,EAAE,GAAG,UAAU,EAAE,CAAC;QACrG,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,OAAO,CAAC,OAAgC;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,OAAO,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC/G,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAmC,EAAE,cAAuB;IAC1F,MAAM,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC1B,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,IAAI,IAAI,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;QAAE,IAAI,EAAE,CAAC;IAChD,IAAI,IAAI,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,CAAC;IAC3C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAA6B,EAAE,GAAiB;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,aAAa,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,GAA4B,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC;IACxD,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,QAAqC,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACpG,CAAC;IACD,sFAAsF;IACtF,IAAI,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;QAC3E,GAAG,CAAC,QAAQ,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC7C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,KAAK,EAAE,YAAY;IACnB,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,oBAAoB;IAC/B,YAAY,EAAE,aAAa;IAC3B,eAAe;IACf,UAAU;IACV,iBAAiB,EAAE,sBAAsB;IACzC,OAAO,EAAE,kBAAkB;IAC3B,UAAU;CACX,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAiB,EAAE,UAAuB,EAAE;IACzE,OAAO,cAAc,CAAC,kBAAkB,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAiB,EAAE,UAAyB,EAAE;IAC7E,OAAO,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AACpF,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* endpoint.ts — where OpenRouter lives and how Mindweave introduces itself to it.
|
|
3
|
+
*
|
|
4
|
+
* Its own module so the catalogue and the client share it without the catalogue
|
|
5
|
+
* loading any wire code.
|
|
6
|
+
*/
|
|
7
|
+
export const BASE_URL = (process.env.MINDWEAVE_OPENROUTER_URL ?? "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
8
|
+
/**
|
|
9
|
+
* App attribution. OpenRouter credits requests to an app by `HTTP-Referer` plus a title,
|
|
10
|
+
* which is what lists it on openrouter.ai's app rankings; the `User-Agent` every driver
|
|
11
|
+
* already sends is not read for that. `X-Title` is the older spelling of the title and
|
|
12
|
+
* still accepted, so both go out.
|
|
13
|
+
*/
|
|
14
|
+
export const OPENROUTER_HEADERS = {
|
|
15
|
+
"HTTP-Referer": "https://github.com/mindweave-cli/mindweave",
|
|
16
|
+
"X-OpenRouter-Title": "mwcode",
|
|
17
|
+
"X-Title": "mwcode",
|
|
18
|
+
"X-OpenRouter-Categories": "cli-agent",
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=endpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../../src/drivers/openrouter/endpoint.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,8BAA8B,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAErH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA2B;IACxD,cAAc,EAAE,4CAA4C;IAC5D,oBAAoB,EAAE,QAAQ;IAC9B,SAAS,EAAE,QAAQ;IACnB,yBAAyB,EAAE,WAAW;CACvC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/drivers/openrouter/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,CAAC,MAAM,gBAAgB,GAAW,EAAE,GAAG,kBAAkB,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAExF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** The namespace every OpenRouter model id carries inside Mindweave. */
|
|
2
|
+
export const PREFIX = "openrouter:";
|
|
3
|
+
/** OpenRouter's own id for a namespaced one. */
|
|
4
|
+
export function wireId(model) {
|
|
5
|
+
return model.startsWith(PREFIX) ? model.slice(PREFIX.length) : model;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Shown before discovery has run. The same model Mindweave defaults to elsewhere, so a
|
|
9
|
+
* fresh OpenRouter user starts somewhere familiar and cheap. A fallback, never a
|
|
10
|
+
* placement: the full catalogue replaces this list as soon as it arrives.
|
|
11
|
+
*/
|
|
12
|
+
export const MODELS = [
|
|
13
|
+
{ id: `${PREFIX}deepseek/deepseek-v4.1-flash`, label: "DeepSeek V4.1 Flash", description: "DeepSeek" },
|
|
14
|
+
];
|
|
15
|
+
export const DEFAULT_MODEL = MODELS[0].id;
|
|
16
|
+
/**
|
|
17
|
+
* Before the catalogue describes a model, offer the two states every reasoning model
|
|
18
|
+
* on OpenRouter accepts. The catalogue's real ladder replaces this per model.
|
|
19
|
+
*/
|
|
20
|
+
export function thinkLevels(_model) {
|
|
21
|
+
return [
|
|
22
|
+
{ label: "Standard", description: "answer directly — fastest", thinking: false, effort: "low" },
|
|
23
|
+
{ label: "Thinking", description: "think first, then answer", thinking: true, effort: "high" },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
/** DeepSeek V4.1 Flash's catalogue price: the only model a fallback can name. */
|
|
27
|
+
const FALLBACK_PRICE = { cacheHit: 0.003, cacheMiss: 0.15, output: 0.6 };
|
|
28
|
+
export function price(_model) {
|
|
29
|
+
return FALLBACK_PRICE;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The most context Mindweave lets a transcript grow into on this provider.
|
|
33
|
+
*
|
|
34
|
+
* The catalogue reports each model's ADVERTISED maximum, often 1M. The window core
|
|
35
|
+
* wants is the USABLE one, where retrieval stays reliable, and every vendor driver
|
|
36
|
+
* sets that well below the advertised figure (DeepSeek 192K-256K, Gemini 200K) on
|
|
37
|
+
* evidence that recall falls off past it. A router cannot measure hundreds of models,
|
|
38
|
+
* so it applies the same ceiling, and a smaller advertised window still wins.
|
|
39
|
+
*/
|
|
40
|
+
export const USABLE_WINDOW_CAP = 200_000;
|
|
41
|
+
export function contextWindow(_model) {
|
|
42
|
+
return 128_000;
|
|
43
|
+
}
|
|
44
|
+
/** The ceiling on a buffered call. Generous, because most models here reason first and
|
|
45
|
+
* a reasoning budget comes out of the same allowance as the answer. */
|
|
46
|
+
export const BUFFERED_OUTPUT_TOKENS = 16_000;
|
|
47
|
+
export function bufferedOutputTokens(_model) {
|
|
48
|
+
return BUFFERED_OUTPUT_TOKENS;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Keep the saved model even when it is not listed yet: the catalogue may not have
|
|
52
|
+
* arrived, and coercing a valid choice onto the fallback would change it on every
|
|
53
|
+
* launch. The registry snaps the reasoning selection onto the model's real ladder once
|
|
54
|
+
* the catalogue describes it.
|
|
55
|
+
*/
|
|
56
|
+
export function normalize(config) {
|
|
57
|
+
const model = config.model || DEFAULT_MODEL;
|
|
58
|
+
const thinking = config.thinking === true;
|
|
59
|
+
// Onto the fallback ladder above: one effort per state.
|
|
60
|
+
return { model, thinking, effort: thinking ? "high" : "low" };
|
|
61
|
+
}
|
|
62
|
+
export function ownsModel(model) {
|
|
63
|
+
return model.startsWith(PREFIX);
|
|
64
|
+
}
|
|
65
|
+
export const openrouterManifest = {
|
|
66
|
+
id: "openrouter",
|
|
67
|
+
label: "OpenRouter",
|
|
68
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
69
|
+
keysUrl: "https://openrouter.ai/keys",
|
|
70
|
+
models: MODELS,
|
|
71
|
+
thinkLevels,
|
|
72
|
+
price,
|
|
73
|
+
contextWindow,
|
|
74
|
+
bufferedOutputTokens,
|
|
75
|
+
normalize,
|
|
76
|
+
ownsModel,
|
|
77
|
+
notice: "OpenRouter may send prompts to hosts that store or train on them. " +
|
|
78
|
+
"Set MINDWEAVE_OPENROUTER_DATA=deny to use only hosts that do not.",
|
|
79
|
+
// Lazy for the same reason as every discovered provider: the manifest loads for every
|
|
80
|
+
// user, and the catalogue code should only load for someone who uses it.
|
|
81
|
+
discoverModels: async () => (await import("./catalog.js")).discoverModels(),
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/drivers/openrouter/manifest.ts"],"names":[],"mappings":"AAiBA,wEAAwE;AACxE,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC;AAEpC,gDAAgD;AAChD,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,MAAM,GAAkB;IACnC,EAAE,EAAE,EAAE,GAAG,MAAM,8BAA8B,EAAE,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,UAAU,EAAE;CACvG,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,MAAe;IACzC,OAAO;QACL,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,2BAA2B,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QAC/F,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,0BAA0B,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE;KAC/F,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,cAAc,GAAe,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAErF,MAAM,UAAU,KAAK,CAAC,MAAe;IACnC,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC;AAEzC,MAAM,UAAU,aAAa,CAAC,MAAe;IAC3C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;wEACwE;AACxE,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAE7C,MAAM,UAAU,oBAAoB,CAAC,MAAe;IAClD,OAAO,sBAAsB,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,MAAmB;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,aAAa,CAAC;IAC5C,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC;IAC1C,wDAAwD;IACxD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,EAAE,EAAE,YAAY;IAChB,KAAK,EAAE,YAAY;IACnB,SAAS,EAAE,oBAAoB;IAC/B,OAAO,EAAE,4BAA4B;IACrC,MAAM,EAAE,MAAM;IACd,WAAW;IACX,KAAK;IACL,aAAa;IACb,oBAAoB;IACpB,SAAS;IACT,SAAS;IACT,MAAM,EACJ,oEAAoE;QACpE,mEAAmE;IACrE,sFAAsF;IACtF,yEAAyE;IACzE,cAAc,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,cAAc,EAAE;CAC5E,CAAC"}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* ACCOUNT reason, and QUOTES the provider's own sentence. The wrapper is universal
|
|
21
21
|
* and cannot be wrong; the precision comes from the provider, which already knows.
|
|
22
22
|
*/
|
|
23
|
+
import { isRetryable } from "./retryPolicy.js";
|
|
23
24
|
/**
|
|
24
25
|
* Statuses that mean the ACCOUNT cannot run this request.
|
|
25
26
|
*
|
|
@@ -178,4 +179,54 @@ export function accessRefusal(error, providerLabel, canSwitch) {
|
|
|
178
179
|
body: said ? `${said}\n\n${action}` : action,
|
|
179
180
|
};
|
|
180
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Recognise a failure on the provider's side and shape it for the screen.
|
|
184
|
+
*
|
|
185
|
+
* By the time one of these reaches the user, the retry layer has already tried again
|
|
186
|
+
* and given up, so the useful thing to say is what the user can do: wait, or pick
|
|
187
|
+
* another model. A red "API error 502: ERROR" says neither, and reads as if Mindweave
|
|
188
|
+
* crashed. Three shapes count:
|
|
189
|
+
* - a 5xx status, the provider or its upstream host falling over;
|
|
190
|
+
* - a failure the provider reported inside a successful response (a router whose
|
|
191
|
+
* upstream died mid-reply), which carries no status;
|
|
192
|
+
* - a request that never got through, after the network retries ran out.
|
|
193
|
+
*
|
|
194
|
+
* A 4xx never lands here. 400 and friends are a request we built wrong and stay loud;
|
|
195
|
+
* 401/402/403/429 are `accessRefusal`'s.
|
|
196
|
+
*
|
|
197
|
+
* The provider's own sentence is quoted when it says something. Routers often send a
|
|
198
|
+
* bare "ERROR" or the status text, and quoting that adds a line that explains nothing.
|
|
199
|
+
*/
|
|
200
|
+
export function providerOutage(error, providerLabel, modelLabel) {
|
|
201
|
+
const status = statusOf(error);
|
|
202
|
+
const upstream = !!error && typeof error === "object" && error.upstream === true;
|
|
203
|
+
const serverSide = status !== null && status >= 500 && status < 600;
|
|
204
|
+
const unreachable = status === null && !upstream && isRetryable(error, null);
|
|
205
|
+
if (!serverSide && !upstream && !unreachable)
|
|
206
|
+
return null;
|
|
207
|
+
const NL = String.fromCharCode(10);
|
|
208
|
+
const advice = "The conversation is saved. Try again in a moment, or pick a different model with /model.";
|
|
209
|
+
if (unreachable) {
|
|
210
|
+
return {
|
|
211
|
+
title: `Can't reach ${providerLabel}`,
|
|
212
|
+
body: `The request didn't get through, even after retrying. Check your connection, or the provider may be down.${NL}${NL}${advice}`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const reported = serverSide
|
|
216
|
+
? providerMessage(detailOf(error))
|
|
217
|
+
: providerMessage(String(error.message ?? "").replace(/^.*?API error:\s*/, ""));
|
|
218
|
+
const informative = reported.length > 0 &&
|
|
219
|
+
!/^(error|internal server error|bad gateway|service unavailable|gateway timeout|the reply ended with an error)\.?$/i.test(reported);
|
|
220
|
+
const what = serverSide
|
|
221
|
+
? `${providerLabel} returned an error (${status}) while running it, even after retrying.`
|
|
222
|
+
: `${providerLabel} reported an error part way through the reply.`;
|
|
223
|
+
const lines = [
|
|
224
|
+
`${what} This is on the provider's side, not your key or your setup. New and preview models fail like this more often.`,
|
|
225
|
+
...(informative ? [`${providerLabel} said: "${reported}"`] : []),
|
|
226
|
+
];
|
|
227
|
+
return {
|
|
228
|
+
title: `${modelLabel} isn't responding right now`,
|
|
229
|
+
body: `${lines.join(NL)}${NL}${NL}${advice}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
181
232
|
//# sourceMappingURL=providerError.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providerError.js","sourceRoot":"","sources":["../../src/drivers/providerError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;
|
|
1
|
+
{"version":3,"file":"providerError.js","sourceRoot":"","sources":["../../src/drivers/providerError.ts"],"names":[],"mappings":"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"}
|
package/dist/drivers/registry.js
CHANGED
|
@@ -12,6 +12,7 @@ import { geminiManifest } from "./gemini/manifest.js";
|
|
|
12
12
|
import { metaManifest } from "./meta/manifest.js";
|
|
13
13
|
import { minimaxManifest } from "./minimax/manifest.js";
|
|
14
14
|
import { tencentManifest } from "./tencent/manifest.js";
|
|
15
|
+
import { openrouterManifest } from "./openrouter/manifest.js";
|
|
15
16
|
/** Every provider's cheap metadata, in display order. Always loaded. */
|
|
16
17
|
const MANIFESTS = [
|
|
17
18
|
deepseekManifest,
|
|
@@ -28,6 +29,7 @@ const MANIFESTS = [
|
|
|
28
29
|
metaManifest,
|
|
29
30
|
minimaxManifest,
|
|
30
31
|
tencentManifest,
|
|
32
|
+
openrouterManifest,
|
|
31
33
|
];
|
|
32
34
|
/** How to load each provider's wire code, on demand. Keyed by manifest id. */
|
|
33
35
|
const LOADERS = {
|
|
@@ -45,6 +47,7 @@ const LOADERS = {
|
|
|
45
47
|
meta: async () => (await import("./meta/index.js")).metaDriver,
|
|
46
48
|
minimax: async () => (await import("./minimax/index.js")).minimaxDriver,
|
|
47
49
|
tencent: async () => (await import("./tencent/index.js")).tencentDriver,
|
|
50
|
+
openrouter: async () => (await import("./openrouter/index.js")).openrouterDriver,
|
|
48
51
|
};
|
|
49
52
|
/** The provider used when a model id doesn't match any other. */
|
|
50
53
|
const FALLBACK = MANIFESTS[0];
|
|
@@ -85,10 +88,77 @@ export function modelsOf(manifest) {
|
|
|
85
88
|
* actually serves.
|
|
86
89
|
*/
|
|
87
90
|
export function manifestForModel(model) {
|
|
88
|
-
return (MANIFESTS.find((m) => modelsOf(m).some((c) => c.id === model)) ??
|
|
91
|
+
return withFacts(MANIFESTS.find((m) => modelsOf(m).some((c) => c.id === model)) ??
|
|
89
92
|
MANIFESTS.find((m) => m.ownsModel?.(model) === true) ??
|
|
90
93
|
FALLBACK);
|
|
91
94
|
}
|
|
95
|
+
/** The facts a discovered listing reported for one model, if any. */
|
|
96
|
+
function factsOf(manifest, model) {
|
|
97
|
+
return discovered.get(manifest.id)?.find((c) => c.id === model)?.facts;
|
|
98
|
+
}
|
|
99
|
+
const views = new WeakMap();
|
|
100
|
+
/**
|
|
101
|
+
* A discovered provider's manifest, answering from its listing's facts first.
|
|
102
|
+
*
|
|
103
|
+
* Every consumer reads a model's window, price, vision and reasoning ladder through
|
|
104
|
+
* `manifestForModel`, so wrapping here is what lets a router report real numbers
|
|
105
|
+
* without a single call site learning that discovered facts exist. Anything the
|
|
106
|
+
* listing did not report falls through to the manifest's own function.
|
|
107
|
+
*
|
|
108
|
+
* A fixed provider is returned untouched: it has no listing to consult.
|
|
109
|
+
*/
|
|
110
|
+
function withFacts(manifest) {
|
|
111
|
+
if (!manifest.discoverModels)
|
|
112
|
+
return manifest;
|
|
113
|
+
let view = views.get(manifest);
|
|
114
|
+
if (view)
|
|
115
|
+
return view;
|
|
116
|
+
view = {
|
|
117
|
+
...manifest,
|
|
118
|
+
thinkLevels: (model) => factsOf(manifest, model)?.thinkLevels ?? manifest.thinkLevels(model),
|
|
119
|
+
price: (model) => factsOf(manifest, model)?.price ?? manifest.price(model),
|
|
120
|
+
contextWindow: (model) => factsOf(manifest, model)?.contextWindow ?? manifest.contextWindow(model),
|
|
121
|
+
bufferedOutputTokens: (model) => factsOf(manifest, model)?.bufferedOutputTokens ?? manifest.bufferedOutputTokens?.(model) ?? 0,
|
|
122
|
+
acceptsImages: (model) => factsOf(manifest, model)?.acceptsImages ?? manifest.acceptsImages?.(model) ?? false,
|
|
123
|
+
normalize: (config) => {
|
|
124
|
+
const base = manifest.normalize(config);
|
|
125
|
+
const levels = factsOf(manifest, base.model)?.thinkLevels;
|
|
126
|
+
// Snapped from the caller's own selection, not the manifest's fallback answer: the
|
|
127
|
+
// fallback ladder is coarser, and rounding through it first would lose a rung the
|
|
128
|
+
// real ladder has.
|
|
129
|
+
return levels ? snapToLevels({ ...config, model: base.model }, levels) : base;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
views.set(manifest, view);
|
|
133
|
+
return view;
|
|
134
|
+
}
|
|
135
|
+
const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
136
|
+
/**
|
|
137
|
+
* Move a reasoning selection onto a ladder the model actually offers.
|
|
138
|
+
*
|
|
139
|
+
* Thinking is kept on or off when the ladder allows it, and flipped when it does not
|
|
140
|
+
* (a model that cannot stop reasoning has no off rung). The effort goes to the nearest
|
|
141
|
+
* offered rung, ties downward, which is the convention every fixed driver follows:
|
|
142
|
+
* rounding up would quietly spend more than the user chose.
|
|
143
|
+
*/
|
|
144
|
+
export function snapToLevels(config, levels) {
|
|
145
|
+
if (levels.length === 0)
|
|
146
|
+
return config;
|
|
147
|
+
const same = levels.filter((l) => l.thinking === config.thinking);
|
|
148
|
+
const pool = same.length > 0 ? same : levels;
|
|
149
|
+
const thinking = pool[0].thinking;
|
|
150
|
+
if (pool.some((l) => l.effort === config.effort))
|
|
151
|
+
return { ...config, thinking };
|
|
152
|
+
const want = EFFORTS.indexOf(config.effort);
|
|
153
|
+
let best = pool[0];
|
|
154
|
+
for (const level of pool) {
|
|
155
|
+
const d = Math.abs(EFFORTS.indexOf(level.effort) - want);
|
|
156
|
+
const bestD = Math.abs(EFFORTS.indexOf(best.effort) - want);
|
|
157
|
+
if (d < bestD || (d === bestD && EFFORTS.indexOf(level.effort) < EFFORTS.indexOf(best.effort)))
|
|
158
|
+
best = level;
|
|
159
|
+
}
|
|
160
|
+
return { ...config, thinking, effort: best.effort };
|
|
161
|
+
}
|
|
92
162
|
/** Every model offered across all installed providers. */
|
|
93
163
|
export function allModels() {
|
|
94
164
|
return MANIFESTS.flatMap((m) => modelsOf(m));
|
|
@@ -105,14 +175,23 @@ export function allModels() {
|
|
|
105
175
|
* it (no key, nothing running), not by a picker that quietly loses its contents.
|
|
106
176
|
* Returns the ids that refreshed successfully, so a caller can tell the difference.
|
|
107
177
|
*/
|
|
108
|
-
export async function refreshModels() {
|
|
109
|
-
const
|
|
178
|
+
export async function refreshModels(options = {}) {
|
|
179
|
+
const maxAge = options.maxAgeMs ?? 0;
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const dynamic = MANIFESTS.filter((m) => m.discoverModels).filter((m) => {
|
|
182
|
+
// A list younger than the caller's tolerance is left alone. A router's catalogue is
|
|
183
|
+
// a large download, and asking for it on every picker open costs a visible pause
|
|
184
|
+
// for a list that changes weekly.
|
|
185
|
+
const at = refreshedAt.get(m.id);
|
|
186
|
+
return maxAge <= 0 || at === undefined || now - at >= maxAge;
|
|
187
|
+
});
|
|
110
188
|
const results = await Promise.all(dynamic.map(async (m) => {
|
|
111
189
|
try {
|
|
112
190
|
const models = await m.discoverModels();
|
|
113
191
|
// An empty result is a real answer — a runtime with nothing pulled — and is
|
|
114
192
|
// stored as such. Failure is the case that must not overwrite.
|
|
115
193
|
discovered.set(m.id, models);
|
|
194
|
+
refreshedAt.set(m.id, Date.now());
|
|
116
195
|
return m.id;
|
|
117
196
|
}
|
|
118
197
|
catch {
|
|
@@ -121,9 +200,38 @@ export async function refreshModels() {
|
|
|
121
200
|
}));
|
|
122
201
|
return results.filter((id) => id !== null);
|
|
123
202
|
}
|
|
203
|
+
/** When each discovered list was fetched, in ms. Seeded lists carry their fetch time. */
|
|
204
|
+
const refreshedAt = new Map();
|
|
205
|
+
/**
|
|
206
|
+
* A discovered provider's list as last fetched, for persisting. Undefined when it has
|
|
207
|
+
* never been fetched or seeded, which is different from an empty list.
|
|
208
|
+
*/
|
|
209
|
+
export function discoveredList(id) {
|
|
210
|
+
const models = discovered.get(id);
|
|
211
|
+
const fetchedAt = refreshedAt.get(id);
|
|
212
|
+
return models && fetchedAt !== undefined ? { models, fetchedAt } : undefined;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Install a previously fetched list, so a picker and the first turn have real facts
|
|
216
|
+
* before the network answers. Never replaces a list fetched in this process: a disk
|
|
217
|
+
* copy is older by definition.
|
|
218
|
+
*/
|
|
219
|
+
export function seedDiscovered(id, models, fetchedAt) {
|
|
220
|
+
if (!MANIFESTS.some((m) => m.id === id && m.discoverModels))
|
|
221
|
+
return;
|
|
222
|
+
if (discovered.has(id))
|
|
223
|
+
return;
|
|
224
|
+
discovered.set(id, models);
|
|
225
|
+
refreshedAt.set(id, fetchedAt);
|
|
226
|
+
}
|
|
227
|
+
/** Ids of every provider that discovers its models. */
|
|
228
|
+
export function discoveredProviderIds() {
|
|
229
|
+
return MANIFESTS.filter((m) => m.discoverModels).map((m) => m.id);
|
|
230
|
+
}
|
|
124
231
|
/** Drop every discovered list. For tests, and for a full provider reset. */
|
|
125
232
|
export function clearDiscovered() {
|
|
126
233
|
discovered.clear();
|
|
234
|
+
refreshedAt.clear();
|
|
127
235
|
}
|
|
128
236
|
/**
|
|
129
237
|
* Every installed provider, in display order — what `/provider` lists.
|