@nvae/llmswitch 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -0
- package/dist/adapters/opencode.js +22 -2
- package/dist/bridge/anthropic-to-chat-response.js +332 -0
- package/dist/bridge/chat-to-anthropic-request.js +270 -0
- package/dist/bridge/chat-to-responses-request.js +216 -0
- package/dist/bridge/responses-to-chat-response.js +393 -0
- package/dist/cli.js +2 -0
- package/dist/commands/gateway-cmd.js +1040 -0
- package/dist/commands/prompts.js +63 -18
- package/dist/commands/tool.js +1 -0
- package/dist/gateway/health.js +45 -0
- package/dist/gateway/keys.js +433 -0
- package/dist/gateway/manager.js +278 -0
- package/dist/gateway/pipeline.js +328 -0
- package/dist/gateway/rate-limit.js +285 -0
- package/dist/gateway/router.js +163 -0
- package/dist/gateway/runtime.js +45 -0
- package/dist/gateway/server.js +1053 -0
- package/dist/gateway/state.js +135 -0
- package/dist/gateway/store.js +392 -0
- package/dist/gateway/tokens.js +423 -0
- package/dist/gateway/types.js +30 -0
- package/dist/gateway/usage.js +152 -0
- package/dist/store/profiles.js +10 -0
- package/dist/utils/model-metadata.js +179 -0
- package/dist/utils/paths.js +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { requestWithNodeTransport } from "../bridge/transport.js";
|
|
2
|
+
export const MODEL_METADATA_SOURCE = "https://models.lonae.com";
|
|
3
|
+
const DEFAULT_ENDPOINT = `${MODEL_METADATA_SOURCE}/api/v1/models`;
|
|
4
|
+
const PAGE_SIZE = 1000;
|
|
5
|
+
/** Trailing date stamp, e.g. "-20250219" in "claude-3-7-sonnet-20250219". */
|
|
6
|
+
const DATE_SUFFIX_RE = /-?\d{8}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a model id for fuzzy matching: lowercase, drop separators, and
|
|
9
|
+
* align dotted version segments ("claude-3.7-sonnet" ↔ "claude-3-7-sonnet").
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeModelKey(value) {
|
|
12
|
+
return value
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/\./g, "-")
|
|
15
|
+
.replace(/[^a-z0-9]/g, "");
|
|
16
|
+
}
|
|
17
|
+
/** Normalize a model id ignoring a trailing date stamp. */
|
|
18
|
+
export function normalizeModelKeyLoose(value) {
|
|
19
|
+
const bare = value.replace(DATE_SUFFIX_RE, "");
|
|
20
|
+
return normalizeModelKey(bare) || normalizeModelKey(value);
|
|
21
|
+
}
|
|
22
|
+
function asModalityList(value) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
return undefined;
|
|
25
|
+
const list = value.filter((item) => typeof item === "string" && !!item.trim());
|
|
26
|
+
return list.length > 0 ? list : undefined;
|
|
27
|
+
}
|
|
28
|
+
function parseModalities(row) {
|
|
29
|
+
const nested = row.modalities;
|
|
30
|
+
const input = (nested && typeof nested === "object"
|
|
31
|
+
? asModalityList(nested.input)
|
|
32
|
+
: undefined) ?? asModalityList(row.inputModalities);
|
|
33
|
+
const output = (nested && typeof nested === "object"
|
|
34
|
+
? asModalityList(nested.output)
|
|
35
|
+
: undefined) ?? asModalityList(row.outputModalities);
|
|
36
|
+
if (!input && !output)
|
|
37
|
+
return undefined;
|
|
38
|
+
return {
|
|
39
|
+
input: input ?? ["text"],
|
|
40
|
+
output: output ?? ["text"],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Insert into a normalized bucket, preferring undated ids as canonical. */
|
|
44
|
+
function indexNorm(bucket, datedMetas, key, meta, dated) {
|
|
45
|
+
if (!key)
|
|
46
|
+
return;
|
|
47
|
+
const existing = bucket[key];
|
|
48
|
+
if (!existing) {
|
|
49
|
+
bucket[key] = meta;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (datedMetas.has(existing) && !dated)
|
|
53
|
+
bucket[key] = meta;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the lookup catalog from a /api/v1/models payload.
|
|
57
|
+
* Indexes: full id ("lab/model"), bare id ("model"), plus normalized variants
|
|
58
|
+
* (date-stamp tolerant) so "claude-3.7-sonnet" can find
|
|
59
|
+
* "claude-3-7-sonnet-20250219".
|
|
60
|
+
*/
|
|
61
|
+
export function parseModelMetadata(payload) {
|
|
62
|
+
const catalog = { full: {}, bare: {}, norm: {} };
|
|
63
|
+
const rows = payload && typeof payload === "object"
|
|
64
|
+
? payload.data
|
|
65
|
+
: null;
|
|
66
|
+
if (!Array.isArray(rows))
|
|
67
|
+
return catalog;
|
|
68
|
+
const datedMetas = new Set();
|
|
69
|
+
for (const item of rows) {
|
|
70
|
+
if (!item || typeof item !== "object")
|
|
71
|
+
continue;
|
|
72
|
+
const row = item;
|
|
73
|
+
if (typeof row.id !== "string" || !row.id.trim())
|
|
74
|
+
continue;
|
|
75
|
+
const id = row.id.trim();
|
|
76
|
+
const meta = {
|
|
77
|
+
id,
|
|
78
|
+
name: typeof row.name === "string" && row.name.trim() ? row.name.trim() : undefined,
|
|
79
|
+
modalities: parseModalities(row),
|
|
80
|
+
attachment: typeof row.attachment === "boolean" ? row.attachment : undefined,
|
|
81
|
+
};
|
|
82
|
+
const bareIndex = id.indexOf("/");
|
|
83
|
+
const bare = bareIndex >= 0 ? id.slice(bareIndex + 1).trim() : id;
|
|
84
|
+
const dated = DATE_SUFFIX_RE.test(bare);
|
|
85
|
+
if (dated)
|
|
86
|
+
datedMetas.add(meta);
|
|
87
|
+
const fullKey = id.toLowerCase();
|
|
88
|
+
const bareKey = bare.toLowerCase();
|
|
89
|
+
const normKey = normalizeModelKey(bare);
|
|
90
|
+
const looseKey = normalizeModelKeyLoose(bare);
|
|
91
|
+
if (!catalog.full[fullKey])
|
|
92
|
+
catalog.full[fullKey] = meta;
|
|
93
|
+
if (bare && !catalog.bare[bareKey])
|
|
94
|
+
catalog.bare[bareKey] = meta;
|
|
95
|
+
indexNorm(catalog.norm, datedMetas, normKey, meta, dated);
|
|
96
|
+
if (looseKey !== normKey)
|
|
97
|
+
indexNorm(catalog.norm, datedMetas, looseKey, meta, dated);
|
|
98
|
+
}
|
|
99
|
+
return catalog;
|
|
100
|
+
}
|
|
101
|
+
/** Look up metadata for a profile model id (exact → bare → normalized). */
|
|
102
|
+
export function lookupModelMeta(catalog, modelId) {
|
|
103
|
+
const trimmed = modelId.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
return undefined;
|
|
106
|
+
const bareIndex = trimmed.indexOf("/");
|
|
107
|
+
const bare = bareIndex >= 0 ? trimmed.slice(bareIndex + 1).trim() : trimmed;
|
|
108
|
+
return (catalog.full[trimmed.toLowerCase()] ??
|
|
109
|
+
catalog.bare[bare.toLowerCase()] ??
|
|
110
|
+
catalog.norm[normalizeModelKey(bare)] ??
|
|
111
|
+
catalog.norm[normalizeModelKeyLoose(bare)]);
|
|
112
|
+
}
|
|
113
|
+
/** Collect metadata for a set of profile model ids. */
|
|
114
|
+
export function collectModelMeta(catalog, models) {
|
|
115
|
+
if (!catalog)
|
|
116
|
+
return undefined;
|
|
117
|
+
const meta = {};
|
|
118
|
+
for (const id of models) {
|
|
119
|
+
const found = lookupModelMeta(catalog, id);
|
|
120
|
+
if (found)
|
|
121
|
+
meta[id] = found;
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch the model metadata catalog from models.lonae.com.
|
|
127
|
+
* Paginates automatically (page_size capped at 1000 per request).
|
|
128
|
+
*/
|
|
129
|
+
export async function fetchModelMetadata(options = {}) {
|
|
130
|
+
const endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
131
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
132
|
+
const rows = [];
|
|
133
|
+
let total = Infinity;
|
|
134
|
+
let page = 1;
|
|
135
|
+
while (rows.length < total) {
|
|
136
|
+
const url = `${endpoint}${endpoint.includes("?") ? "&" : "?"}page_size=${PAGE_SIZE}&page=${page}`;
|
|
137
|
+
const payload = await requestJson(url, options.proxy, timeoutMs);
|
|
138
|
+
const batch = payload && typeof payload === "object"
|
|
139
|
+
? payload.data
|
|
140
|
+
: null;
|
|
141
|
+
if (!Array.isArray(batch) || batch.length === 0)
|
|
142
|
+
break;
|
|
143
|
+
rows.push(...batch);
|
|
144
|
+
const totalRaw = payload && typeof payload === "object"
|
|
145
|
+
? payload.meta?.total
|
|
146
|
+
: undefined;
|
|
147
|
+
total = typeof totalRaw === "number" && totalRaw > 0 ? totalRaw : rows.length;
|
|
148
|
+
page += 1;
|
|
149
|
+
}
|
|
150
|
+
return parseModelMetadata({ data: rows });
|
|
151
|
+
}
|
|
152
|
+
async function requestJson(url, proxy, timeoutMs) {
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
155
|
+
try {
|
|
156
|
+
const res = await requestWithNodeTransport({
|
|
157
|
+
url,
|
|
158
|
+
method: "GET",
|
|
159
|
+
headers: { Accept: "application/json" },
|
|
160
|
+
proxy,
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
totalTimeoutMs: timeoutMs,
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
166
|
+
throw new Error(`HTTP ${res.status}${body ? `: ${body}` : ""}`);
|
|
167
|
+
}
|
|
168
|
+
return await res.json();
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
172
|
+
throw new Error(`请求超时(${timeoutMs}ms)`);
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/utils/paths.js
CHANGED
|
@@ -26,6 +26,30 @@ export function getStatePath(tool) {
|
|
|
26
26
|
export function getBackupsDir(tool) {
|
|
27
27
|
return join(getToolStoreDir(tool), "backups");
|
|
28
28
|
}
|
|
29
|
+
export function getGatewayDir() {
|
|
30
|
+
return join(getAppConfigRoot(), "gateway");
|
|
31
|
+
}
|
|
32
|
+
export function getGatewayProvidersDir() {
|
|
33
|
+
return join(getGatewayDir(), "providers");
|
|
34
|
+
}
|
|
35
|
+
export function getGatewayProviderPath(name) {
|
|
36
|
+
return join(getGatewayProvidersDir(), `${name}.json`);
|
|
37
|
+
}
|
|
38
|
+
export function getGatewayRoutesPath() {
|
|
39
|
+
return join(getGatewayDir(), "routes.json");
|
|
40
|
+
}
|
|
41
|
+
export function getGatewayConfigPath() {
|
|
42
|
+
return join(getGatewayDir(), "config.json");
|
|
43
|
+
}
|
|
44
|
+
export function getGatewayKeysPath() {
|
|
45
|
+
return join(getGatewayDir(), "keys.json");
|
|
46
|
+
}
|
|
47
|
+
export function getGatewayUsagePath() {
|
|
48
|
+
return join(getGatewayDir(), "usage.json");
|
|
49
|
+
}
|
|
50
|
+
export function getGatewayStatePath() {
|
|
51
|
+
return join(getGatewayDir(), "state.json");
|
|
52
|
+
}
|
|
29
53
|
export function getClaudeConfigDir() {
|
|
30
54
|
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
31
55
|
}
|