om-memory-system 6.2.0-next.243 → 6.3.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/dist/config.js +3 -0
- package/dist/core/memorisation-operations.js +2 -0
- package/dist/importer/external-api-test.d.ts +4 -2
- package/dist/importer/external-api-test.js +3 -2
- package/dist/services/ai/provider-config.d.ts +1 -1
- package/dist/services/ai/provider-error-kind.d.ts +7 -0
- package/dist/services/ai/provider-error-kind.js +9 -0
- package/dist/services/ai/providers/anthropic-messages.js +3 -2
- package/dist/services/ai/providers/base-provider.d.ts +2 -0
- package/dist/services/ai/providers/base-provider.js +4 -0
- package/dist/services/ai/providers/google-gemini.js +3 -2
- package/dist/services/ai/providers/openai-chat-completion.js +3 -2
- package/dist/services/ai/providers/openai-responses.js +3 -2
- package/dist/services/api-handlers.d.ts +3 -24
- package/dist/services/api-handlers.js +4 -114
- package/dist/services/client.js +1 -0
- package/dist/services/global-config-writer.d.ts +12 -5
- package/dist/services/global-config-writer.js +23 -8
- package/dist/services/linked-search.d.ts +2 -0
- package/dist/services/openrouter-models.d.ts +25 -0
- package/dist/services/openrouter-models.js +58 -0
- package/dist/services/openrouter-providers.d.ts +37 -0
- package/dist/services/openrouter-providers.js +147 -0
- package/dist/services/openrouter-routes.d.ts +9 -0
- package/dist/services/openrouter-routes.js +77 -0
- package/dist/services/openrouter-routing-config.d.ts +48 -0
- package/dist/services/openrouter-routing-config.js +143 -0
- package/dist/services/retrieval-rerank.d.ts +2 -0
- package/dist/services/retrieval-rerank.js +4 -1
- package/dist/services/settings-snapshot.d.ts +1 -0
- package/dist/services/settings-snapshot.js +5 -0
- package/dist/services/tag-migration.d.ts +48 -0
- package/dist/services/tag-migration.js +183 -0
- package/dist/services/web-route-modules.js +4 -0
- package/dist/web/assets/index-4epIq1xy.js +171 -0
- package/dist/web/assets/index-Tc6q6ju1.css +2 -0
- package/dist/web/assets/react-SIfiwpqq.js +1 -0
- package/dist/web/index.html +3 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CoYuPYCS.css +0 -2
- package/dist/web/assets/index-jbZuE9HV.js +0 -171
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The providers that serve one OpenRouter model, each with a data policy label.
|
|
3
|
+
* It reads two public OpenRouter lists without an API key: the model's endpoints
|
|
4
|
+
* and the zero data retention (ZDR) endpoints. `fetch` and the clock come in as
|
|
5
|
+
* arguments, so the module needs no config and tests need no network.
|
|
6
|
+
*/
|
|
7
|
+
const BASE = "https://openrouter.ai/api/v1";
|
|
8
|
+
const ZDR_TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
10
|
+
const MODEL_ID = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._:-]+$/;
|
|
11
|
+
/** From least private to most private is unknown, safety, zdr. */
|
|
12
|
+
const PRIVACY_RANK = { unknown: 0, safety: 1, zdr: 2 };
|
|
13
|
+
/** True for `author/slug` model IDs. Dot-only parts are refused so the URL cannot climb out of /models/. */
|
|
14
|
+
export function isModelId(value) {
|
|
15
|
+
return (typeof value === "string" &&
|
|
16
|
+
MODEL_ID.test(value) &&
|
|
17
|
+
value.split("/").every((part) => !/^\.+$/.test(part)));
|
|
18
|
+
}
|
|
19
|
+
class ListError extends Error {
|
|
20
|
+
reason;
|
|
21
|
+
constructor(reason, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const text = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
27
|
+
/** A per-token price string such as "0.000000256" as US dollars per 1M tokens. Missing, non-numeric or negative gives undefined. */
|
|
28
|
+
function pricePerMillion(value) {
|
|
29
|
+
if (typeof value !== "string" || !value.trim())
|
|
30
|
+
return undefined;
|
|
31
|
+
const perToken = Number(value);
|
|
32
|
+
if (!Number.isFinite(perToken) || perToken < 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
return Math.round(perToken * 1_000_000 * 1e6) / 1e6;
|
|
35
|
+
}
|
|
36
|
+
const lowest = (a, b) => a === undefined ? b : b === undefined ? a : Math.min(a, b);
|
|
37
|
+
export function createOpenRouterProviders(deps) {
|
|
38
|
+
const timeoutMs = deps.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
39
|
+
let zdrCache;
|
|
40
|
+
/** One keyless GET. Returns undefined for a 404, so the caller can name the model. */
|
|
41
|
+
async function getJson(url) {
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await deps.fetch(url, {
|
|
45
|
+
method: "GET",
|
|
46
|
+
headers: { Accept: "application/json" },
|
|
47
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const name = error instanceof Error ? error.name : "";
|
|
52
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
53
|
+
throw new ListError("timeout", "OpenRouter did not answer within 10 seconds.");
|
|
54
|
+
}
|
|
55
|
+
throw new ListError("unreachable", "OpenRouter could not be reached.");
|
|
56
|
+
}
|
|
57
|
+
if (response.status === 404)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new ListError("unreachable", `OpenRouter answered with status ${response.status}.`);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await response.json();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new ListError("unreachable", "OpenRouter sent a reply that could not be read.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** `model_id` + `tag` -> safety_retention for every ZDR endpoint. Cached for an hour. */
|
|
70
|
+
async function zdrFlags() {
|
|
71
|
+
if (zdrCache && deps.now() - zdrCache.at < ZDR_TTL_MS)
|
|
72
|
+
return zdrCache.flags;
|
|
73
|
+
const body = (await getJson(`${BASE}/endpoints/zdr`));
|
|
74
|
+
if (!body || !Array.isArray(body.data)) {
|
|
75
|
+
throw new ListError("unreachable", "OpenRouter sent a zero data retention list in an unknown form.");
|
|
76
|
+
}
|
|
77
|
+
const flags = new Map();
|
|
78
|
+
for (const row of body.data) {
|
|
79
|
+
const model = text(row?.model_id);
|
|
80
|
+
const tag = text(row?.tag);
|
|
81
|
+
// A missing or non-boolean flag is not stored, so the endpoint stays `unknown`.
|
|
82
|
+
if (model && tag && typeof row.safety_retention === "boolean") {
|
|
83
|
+
flags.set(`${model}\n${tag}`, row.safety_retention);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Only a good answer is cached, so a failed fetch is tried again on the next request.
|
|
87
|
+
zdrCache = { at: deps.now(), flags };
|
|
88
|
+
return flags;
|
|
89
|
+
}
|
|
90
|
+
async function list(model) {
|
|
91
|
+
try {
|
|
92
|
+
const [body, flags] = await Promise.all([
|
|
93
|
+
getJson(`${BASE}/models/${model.split("/").map(encodeURIComponent).join("/")}/endpoints`),
|
|
94
|
+
zdrFlags(),
|
|
95
|
+
]);
|
|
96
|
+
const endpoints = body?.data?.endpoints;
|
|
97
|
+
if (body === undefined || (Array.isArray(endpoints) && endpoints.length === 0)) {
|
|
98
|
+
throw new ListError("unknown-model", "OpenRouter has no providers for that model.");
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(endpoints)) {
|
|
101
|
+
throw new ListError("unreachable", "OpenRouter sent a provider list in an unknown form.");
|
|
102
|
+
}
|
|
103
|
+
const bySlug = new Map();
|
|
104
|
+
// Lowest input and output price per slug, found independently across its endpoints.
|
|
105
|
+
const prices = new Map();
|
|
106
|
+
for (const endpoint of endpoints) {
|
|
107
|
+
const tag = text(endpoint?.tag);
|
|
108
|
+
const name = text(endpoint?.provider_name);
|
|
109
|
+
const slug = tag ? tag.split("/")[0] : name?.toLowerCase();
|
|
110
|
+
if (!slug)
|
|
111
|
+
continue;
|
|
112
|
+
const modelId = text(endpoint?.model_id);
|
|
113
|
+
const flag = tag && modelId ? flags.get(`${modelId}\n${tag}`) : undefined;
|
|
114
|
+
const policy = flag === undefined ? "unknown" : flag ? "safety" : "zdr";
|
|
115
|
+
const pricing = endpoint?.pricing;
|
|
116
|
+
const lows = prices.get(slug) ?? {};
|
|
117
|
+
lows.input = lowest(lows.input, pricePerMillion(pricing?.prompt));
|
|
118
|
+
lows.output = lowest(lows.output, pricePerMillion(pricing?.completion));
|
|
119
|
+
prices.set(slug, lows);
|
|
120
|
+
const seen = bySlug.get(slug);
|
|
121
|
+
if (!seen) {
|
|
122
|
+
bySlug.set(slug, { slug, name: name ?? slug, policy });
|
|
123
|
+
}
|
|
124
|
+
else if (PRIVACY_RANK[policy] < PRIVACY_RANK[seen.policy]) {
|
|
125
|
+
seen.policy = policy;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// A price needs both values. A slug with only one of them shows no price.
|
|
129
|
+
for (const [slug, row] of bySlug) {
|
|
130
|
+
const { input, output } = prices.get(slug) ?? {};
|
|
131
|
+
if (input !== undefined && output !== undefined)
|
|
132
|
+
row.price = { input, output };
|
|
133
|
+
}
|
|
134
|
+
const providers = [...bySlug.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
if (providers.length === 0) {
|
|
136
|
+
throw new ListError("unknown-model", "OpenRouter has no providers for that model.");
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, providers };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof ListError)
|
|
142
|
+
return { ok: false, reason: error.reason, error: error.message };
|
|
143
|
+
return { ok: false, reason: "unreachable", error: "OpenRouter could not be reached." };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { list };
|
|
147
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RouteResult } from "./chat/chat-routes.js";
|
|
2
|
+
/**
|
|
3
|
+
* The OpenRouter routing routes of the External API card: the provider list of a
|
|
4
|
+
* model with data policy labels, and the save of `memoryExtraParams.provider`.
|
|
5
|
+
* Returns undefined for paths it does not own.
|
|
6
|
+
*/
|
|
7
|
+
export declare function routeOpenRouter(path: string, method: string, url: URL, readBody: () => Promise<any>, ctx: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
}): Promise<RouteResult | undefined>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
import { createOpenRouterModels } from "./openrouter-models.js";
|
|
3
|
+
import { createOpenRouterProviders, isModelId } from "./openrouter-providers.js";
|
|
4
|
+
import { isOpenRouterUrl, parseRouting, removeProviderEdits, routingEdits, } from "./openrouter-routing-config.js";
|
|
5
|
+
const PROVIDERS_PATH = "/api/settings/external-api/openrouter-providers";
|
|
6
|
+
const MODELS_PATH = "/api/settings/external-api/openrouter-models";
|
|
7
|
+
const ROUTING_PATH = "/api/settings/external-api/openrouter-routing";
|
|
8
|
+
/** Reads `globalThis.fetch` at call time, so a test can replace it. */
|
|
9
|
+
const providers = createOpenRouterProviders({
|
|
10
|
+
fetch: (input, init) => globalThis.fetch(input, init),
|
|
11
|
+
now: () => Date.now(),
|
|
12
|
+
});
|
|
13
|
+
const models = createOpenRouterModels({
|
|
14
|
+
fetch: (input, init) => globalThis.fetch(input, init),
|
|
15
|
+
now: () => Date.now(),
|
|
16
|
+
});
|
|
17
|
+
const NOT_OPENROUTER = "Save the OpenRouter endpoint first. Routing applies only to an OpenRouter API URL.";
|
|
18
|
+
const fail = (status, error) => ({ status, body: { error } });
|
|
19
|
+
/**
|
|
20
|
+
* The OpenRouter routing routes of the External API card: the provider list of a
|
|
21
|
+
* model with data policy labels, and the save of `memoryExtraParams.provider`.
|
|
22
|
+
* Returns undefined for paths it does not own.
|
|
23
|
+
*/
|
|
24
|
+
export async function routeOpenRouter(path, method, url, readBody, ctx) {
|
|
25
|
+
if (path === PROVIDERS_PATH && method === "GET") {
|
|
26
|
+
const model = url.searchParams.get("model");
|
|
27
|
+
if (!isModelId(model)) {
|
|
28
|
+
return fail(400, "Enter a model ID in the form author/model, for example deepseek/deepseek-chat");
|
|
29
|
+
}
|
|
30
|
+
return { status: 200, body: await providers.list(model) };
|
|
31
|
+
}
|
|
32
|
+
if (path === MODELS_PATH && method === "GET") {
|
|
33
|
+
return { status: 200, body: await models.list() };
|
|
34
|
+
}
|
|
35
|
+
if (path === ROUTING_PATH && method === "POST") {
|
|
36
|
+
const body = await readBody();
|
|
37
|
+
if (typeof body?.revision !== "string")
|
|
38
|
+
return fail(400, "Revision required");
|
|
39
|
+
// `routing: null` removes the whole provider object. `{}` clears the managed fields only.
|
|
40
|
+
const removeAll = body.routing === null;
|
|
41
|
+
const parsed = removeAll ? { ok: true, routing: {} } : parseRouting(body.routing);
|
|
42
|
+
if (!parsed.ok)
|
|
43
|
+
return fail(400, parsed.error);
|
|
44
|
+
const { writeGlobalConfigKeys, ConfigConflictError } = await import("./global-config-writer.js");
|
|
45
|
+
let provider;
|
|
46
|
+
let revision;
|
|
47
|
+
try {
|
|
48
|
+
({ revision } = await writeGlobalConfigKeys({}, body.revision, {
|
|
49
|
+
only: new Set(["memoryExtraParams", "memoryExtraParams.provider"]),
|
|
50
|
+
derive: (current) => {
|
|
51
|
+
// A clearing save is always allowed, so a stale provider can go after a switch of endpoint.
|
|
52
|
+
if (Object.keys(parsed.routing).length > 0 && !isOpenRouterUrl(current.memoryApiUrl)) {
|
|
53
|
+
throw new Error(NOT_OPENROUTER);
|
|
54
|
+
}
|
|
55
|
+
if (removeAll)
|
|
56
|
+
return removeProviderEdits(current.memoryExtraParams);
|
|
57
|
+
const result = routingEdits(current.memoryExtraParams, parsed.routing);
|
|
58
|
+
provider = result.provider;
|
|
59
|
+
return result.edits;
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return fail(error instanceof ConfigConflictError ? 409 : 400, error instanceof Error ? error.message : "Routing not saved");
|
|
65
|
+
}
|
|
66
|
+
// The file is already saved; a reload problem must not report the save as failed.
|
|
67
|
+
try {
|
|
68
|
+
const { refreshConfigIfChanged } = await import("../config.js");
|
|
69
|
+
refreshConfigIfChanged(ctx.cwd);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
log("Routing saved, but reloading settings failed", { error: error instanceof Error ? error.message : String(error) }, "error");
|
|
73
|
+
}
|
|
74
|
+
return { status: 200, body: { revision, provider: provider ?? null } };
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for the OpenRouter routing card: check the posted routing, and
|
|
3
|
+
* turn it into config edits that keep every field the card does not manage.
|
|
4
|
+
*/
|
|
5
|
+
/** The `provider` fields the card owns. Any other field (for example `sort`) is left alone. */
|
|
6
|
+
export declare const MANAGED_PROVIDER_KEYS: readonly ["order", "only", "allow_fallbacks", "data_collection", "zdr"];
|
|
7
|
+
export type RoutingResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
routing: Record<string, unknown>;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
/** Check the posted `routing` object. An empty `order` or `only` list is dropped, since it restricts nothing. */
|
|
15
|
+
export declare function parseRouting(input: unknown): RoutingResult;
|
|
16
|
+
/** True when the URL's host is exactly `openrouter.ai`. Routing applies only to that endpoint. */
|
|
17
|
+
export declare function isOpenRouterUrl(value: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The provider object after the save: the saved object without the five managed
|
|
20
|
+
* fields, plus the posted ones. `undefined` means no provider is left.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeProvider(saved: unknown, routing: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Config edits that remove the whole `provider` object, managed fields or not, and
|
|
25
|
+
* `memoryExtraParams` when nothing else is in it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function removeProviderEdits(savedExtraParams: unknown): Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Config edits for `memoryExtraParams.provider`. The nested write keeps the other
|
|
30
|
+
* members and the file's comments. When no provider is left and nothing else is in
|
|
31
|
+
* `memoryExtraParams`, the whole key goes. Returns the edits and the new provider.
|
|
32
|
+
*/
|
|
33
|
+
export declare function routingEdits(savedExtraParams: unknown, routing: Record<string, unknown>): {
|
|
34
|
+
edits: Record<string, unknown>;
|
|
35
|
+
provider: Record<string, unknown> | undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Keep the global `memoryExtraParams.provider` when a project file sets `memoryExtraParams`.
|
|
39
|
+
* The project object replaces the global one as a whole, which would drop a privacy choice
|
|
40
|
+
* such as `zdr`. Like the endpoint and the key, the routing is global. A project keeps its
|
|
41
|
+
* other members. Pure on purpose: callers pass the file configs and a log function, and
|
|
42
|
+
* `CONFIG` is never imported here, because many tests stub `src/config.js`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function keepGlobalProvider(globalConfig: {
|
|
45
|
+
memoryExtraParams?: unknown;
|
|
46
|
+
}, projectOverrides: {
|
|
47
|
+
memoryExtraParams?: unknown;
|
|
48
|
+
}, log?: (message: string) => void): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for the OpenRouter routing card: check the posted routing, and
|
|
3
|
+
* turn it into config edits that keep every field the card does not manage.
|
|
4
|
+
*/
|
|
5
|
+
/** The `provider` fields the card owns. Any other field (for example `sort`) is left alone. */
|
|
6
|
+
export const MANAGED_PROVIDER_KEYS = [
|
|
7
|
+
"order",
|
|
8
|
+
"only",
|
|
9
|
+
"allow_fallbacks",
|
|
10
|
+
"data_collection",
|
|
11
|
+
"zdr",
|
|
12
|
+
];
|
|
13
|
+
// A provider slug (`deepinfra`) or a full endpoint slug with up to two more segments, such as
|
|
14
|
+
// `google-vertex/us-east5` or `baseten/fp4/us`. OpenRouter accepts both in `order` and `only`.
|
|
15
|
+
const SEGMENT = "[a-z0-9][a-z0-9._-]{0,63}";
|
|
16
|
+
const SLUG = new RegExp(`^${SEGMENT}(?:/${SEGMENT}){0,2}$`, "i");
|
|
17
|
+
const MAX_SLUGS = 20;
|
|
18
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
function slugList(name, value) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return `${name} must be a list of provider slugs`;
|
|
22
|
+
if (value.length > MAX_SLUGS)
|
|
23
|
+
return `${name} can list at most ${MAX_SLUGS} providers`;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const entry of value) {
|
|
26
|
+
if (typeof entry !== "string" || !SLUG.test(entry)) {
|
|
27
|
+
return `${name} has a provider slug that is not valid (letters, digits, dot, dash and underscore, with a slash between parts)`;
|
|
28
|
+
}
|
|
29
|
+
const key = entry.toLowerCase();
|
|
30
|
+
if (seen.has(key))
|
|
31
|
+
return `${name} lists a provider more than once`;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/** Check the posted `routing` object. An empty `order` or `only` list is dropped, since it restricts nothing. */
|
|
37
|
+
export function parseRouting(input) {
|
|
38
|
+
if (!isPlainObject(input))
|
|
39
|
+
return { ok: false, error: "routing must be an object" };
|
|
40
|
+
const routing = {};
|
|
41
|
+
for (const [key, value] of Object.entries(input)) {
|
|
42
|
+
if (key === "data_collection") {
|
|
43
|
+
if (value !== "deny")
|
|
44
|
+
return { ok: false, error: 'data_collection can only be "deny"' };
|
|
45
|
+
routing[key] = value;
|
|
46
|
+
}
|
|
47
|
+
else if (key === "zdr") {
|
|
48
|
+
if (value !== true)
|
|
49
|
+
return { ok: false, error: "zdr can only be true" };
|
|
50
|
+
routing[key] = value;
|
|
51
|
+
}
|
|
52
|
+
else if (key === "allow_fallbacks") {
|
|
53
|
+
if (value !== false)
|
|
54
|
+
return { ok: false, error: "allow_fallbacks can only be false" };
|
|
55
|
+
routing[key] = value;
|
|
56
|
+
}
|
|
57
|
+
else if (key === "order" || key === "only") {
|
|
58
|
+
const list = slugList(key, value);
|
|
59
|
+
if (typeof list === "string")
|
|
60
|
+
return { ok: false, error: list };
|
|
61
|
+
if (list.length > 0)
|
|
62
|
+
routing[key] = list;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return { ok: false, error: `routing has a field that is not supported: ${key}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, routing };
|
|
69
|
+
}
|
|
70
|
+
/** True when the URL's host is exactly `openrouter.ai`. Routing applies only to that endpoint. */
|
|
71
|
+
export function isOpenRouterUrl(value) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
return new URL(value).hostname === "openrouter.ai";
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The provider object after the save: the saved object without the five managed
|
|
83
|
+
* fields, plus the posted ones. `undefined` means no provider is left.
|
|
84
|
+
*/
|
|
85
|
+
export function mergeProvider(saved, routing) {
|
|
86
|
+
const merged = isPlainObject(saved) ? { ...saved } : {};
|
|
87
|
+
for (const key of MANAGED_PROVIDER_KEYS)
|
|
88
|
+
delete merged[key];
|
|
89
|
+
Object.assign(merged, routing);
|
|
90
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Config edits that remove the whole `provider` object, managed fields or not, and
|
|
94
|
+
* `memoryExtraParams` when nothing else is in it.
|
|
95
|
+
*/
|
|
96
|
+
export function removeProviderEdits(savedExtraParams) {
|
|
97
|
+
const extra = isPlainObject(savedExtraParams) ? savedExtraParams : undefined;
|
|
98
|
+
const others = extra ? Object.keys(extra).filter((key) => key !== "provider") : [];
|
|
99
|
+
if (!extra || others.length === 0) {
|
|
100
|
+
return savedExtraParams === undefined ? {} : { memoryExtraParams: undefined };
|
|
101
|
+
}
|
|
102
|
+
return { "memoryExtraParams.provider": undefined };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Config edits for `memoryExtraParams.provider`. The nested write keeps the other
|
|
106
|
+
* members and the file's comments. When no provider is left and nothing else is in
|
|
107
|
+
* `memoryExtraParams`, the whole key goes. Returns the edits and the new provider.
|
|
108
|
+
*/
|
|
109
|
+
export function routingEdits(savedExtraParams, routing) {
|
|
110
|
+
const extra = isPlainObject(savedExtraParams) ? savedExtraParams : undefined;
|
|
111
|
+
const provider = mergeProvider(extra?.provider, routing);
|
|
112
|
+
if (provider) {
|
|
113
|
+
// A saved value that is not an object cannot hold a nested key, so it is replaced.
|
|
114
|
+
return {
|
|
115
|
+
edits: extra
|
|
116
|
+
? { "memoryExtraParams.provider": provider }
|
|
117
|
+
: { memoryExtraParams: { provider } },
|
|
118
|
+
provider,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { edits: removeProviderEdits(savedExtraParams), provider };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Keep the global `memoryExtraParams.provider` when a project file sets `memoryExtraParams`.
|
|
125
|
+
* The project object replaces the global one as a whole, which would drop a privacy choice
|
|
126
|
+
* such as `zdr`. Like the endpoint and the key, the routing is global. A project keeps its
|
|
127
|
+
* other members. Pure on purpose: callers pass the file configs and a log function, and
|
|
128
|
+
* `CONFIG` is never imported here, because many tests stub `src/config.js`.
|
|
129
|
+
*/
|
|
130
|
+
export function keepGlobalProvider(globalConfig, projectOverrides, log) {
|
|
131
|
+
const provider = isPlainObject(globalConfig.memoryExtraParams)
|
|
132
|
+
? globalConfig.memoryExtraParams.provider
|
|
133
|
+
: undefined;
|
|
134
|
+
if (!isPlainObject(provider) || !("memoryExtraParams" in projectOverrides))
|
|
135
|
+
return;
|
|
136
|
+
const own = isPlainObject(projectOverrides.memoryExtraParams)
|
|
137
|
+
? projectOverrides.memoryExtraParams
|
|
138
|
+
: {};
|
|
139
|
+
if ("provider" in own) {
|
|
140
|
+
log?.("Project config cannot change memoryExtraParams.provider; the global routing is used");
|
|
141
|
+
}
|
|
142
|
+
projectOverrides.memoryExtraParams = { ...own, provider };
|
|
143
|
+
}
|
|
@@ -24,6 +24,8 @@ export interface RerankedSearchOptions {
|
|
|
24
24
|
maxMemories: number;
|
|
25
25
|
similarityThreshold: number;
|
|
26
26
|
kind?: MemoryKind;
|
|
27
|
+
/** Drop reranked candidates below `similarityThreshold` before the limit (memorisation recall). */
|
|
28
|
+
thresholdAfterRerank?: boolean;
|
|
27
29
|
}
|
|
28
30
|
export interface RerankedSearchDeps {
|
|
29
31
|
search: SearchAcrossShards;
|
|
@@ -15,7 +15,10 @@ export async function rerankedSearch(options, settings, deps) {
|
|
|
15
15
|
return { results: [], warnings, reranked: false };
|
|
16
16
|
try {
|
|
17
17
|
const { ordered } = await rerankDocuments(settings, options.queryText, candidates, deps.fetchImpl);
|
|
18
|
-
|
|
18
|
+
const kept = options.thresholdAfterRerank
|
|
19
|
+
? ordered.filter((result) => result.similarity >= options.similarityThreshold)
|
|
20
|
+
: ordered;
|
|
21
|
+
return { results: kept.slice(0, options.maxMemories), warnings, reranked: true };
|
|
19
22
|
}
|
|
20
23
|
catch (error) {
|
|
21
24
|
// Metadata only: the code, the elapsed time, and the candidate count.
|
|
@@ -18,6 +18,7 @@ export declare function getSettingsSnapshot(directory: string): {
|
|
|
18
18
|
resolvesInWebApp: boolean;
|
|
19
19
|
warning: string | null;
|
|
20
20
|
};
|
|
21
|
+
openrouterProvider: Record<string, unknown> | null;
|
|
21
22
|
effective: {
|
|
22
23
|
opencode: import("./ai/live-model-choice.js").AutoCaptureProviderStatus;
|
|
23
24
|
pi: import("./ai/live-model-choice.js").PiLiveModelChoice;
|
|
@@ -126,6 +126,9 @@ function nestedSetting(path, defaultValue, global, project) {
|
|
|
126
126
|
hasGlobal,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
const plainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
130
|
+
? value
|
|
131
|
+
: null;
|
|
129
132
|
export function getSettingsSnapshot(directory) {
|
|
130
133
|
const global = readSettingsFile(getGlobalConfigSourcePath());
|
|
131
134
|
const project = readSettingsFile(projectFile(directory));
|
|
@@ -218,6 +221,8 @@ export function getSettingsSnapshot(directory) {
|
|
|
218
221
|
secrets,
|
|
219
222
|
fallback: { model: CONFIG.memoryModel ?? null, configured: isExternalModelReady(CONFIG) },
|
|
220
223
|
externalKey: memoryKeyStatus(global.memoryApiKey),
|
|
224
|
+
// The OpenRouter routing card reads the saved `provider` object of memoryExtraParams.
|
|
225
|
+
openrouterProvider: plainObject(plainObject(global.memoryExtraParams)?.provider),
|
|
221
226
|
effective: {
|
|
222
227
|
opencode: getAutoCaptureProviderStatus(CONFIG),
|
|
223
228
|
pi: resolvePiLiveModel(CONFIG),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ProviderErrorKind } from "./ai/provider-error-kind.js";
|
|
2
|
+
import type { buildMemoryProviderConfig } from "./ai/provider-config.js";
|
|
3
|
+
import type { AIProviderType } from "./ai/session/session-types.js";
|
|
4
|
+
export interface ApiResponse<T> {
|
|
5
|
+
success: boolean;
|
|
6
|
+
data?: T;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export type TagMigrationConfig = Parameters<typeof buildMemoryProviderConfig>[0] & {
|
|
10
|
+
memoryProvider: AIProviderType;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* One batch's answer. `error` and `errorKind` describe the failure that stopped the run,
|
|
14
|
+
* or else the first failure of the run.
|
|
15
|
+
*/
|
|
16
|
+
export interface TagMigrationBatch {
|
|
17
|
+
processed: number;
|
|
18
|
+
total: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
tagged: number;
|
|
21
|
+
failed: number;
|
|
22
|
+
errorKind?: ProviderErrorKind;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
interface MigrationProgress {
|
|
26
|
+
processed: number;
|
|
27
|
+
total: number;
|
|
28
|
+
currentBatch: number;
|
|
29
|
+
totalBatches: number;
|
|
30
|
+
isComplete: boolean;
|
|
31
|
+
tagged: number;
|
|
32
|
+
errors: string[];
|
|
33
|
+
errorKind?: ProviderErrorKind;
|
|
34
|
+
}
|
|
35
|
+
export declare function handleDetectTagMigration(): Promise<ApiResponse<{
|
|
36
|
+
needsMigration: boolean;
|
|
37
|
+
count: number;
|
|
38
|
+
}>>;
|
|
39
|
+
export declare function handleGetTagMigrationProgress(): Promise<ApiResponse<MigrationProgress>>;
|
|
40
|
+
/**
|
|
41
|
+
* Tag the memories that have no tags, a few per request. Only untagged
|
|
42
|
+
* memories are touched: tagged ones keep their vectors. A memory that fails
|
|
43
|
+
* is recorded and passed over, so one bad row cannot stall the run. An error
|
|
44
|
+
* that fails every memory the same way (no credit, a rejected key) ends the
|
|
45
|
+
* run at once, so a later run starts over with every untagged memory.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runTagMigrationBatch(config: TagMigrationConfig, batchSize?: number): Promise<ApiResponse<TagMigrationBatch>>;
|
|
48
|
+
export {};
|