@tokennotincluded/dsh-lmm-provider 0.1.0-alpha.3
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 +64 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +211 -0
- package/lib/index.mjs +1803 -0
- package/package.json +104 -0
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,1803 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { createAssistantMessageEventStream, createModels } from "@earendil-works/pi-ai";
|
|
3
|
+
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
4
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
5
|
+
import { LlmAdapter, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
7
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
|
+
import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { mkdir, open } from "node:fs/promises";
|
|
11
|
+
import { anthropicMessagesApi, openAICompletionsApi, openAIResponsesApi } from "@earendil-works/pi-ai/compat";
|
|
12
|
+
//#region vendor/pi-lmm-provider/src/protocol.ts
|
|
13
|
+
const CLIENT_ID = "lmm-pi";
|
|
14
|
+
const CALLBACK_PATH = "/oauth/lmm/callback";
|
|
15
|
+
const APPLICATION_SCOPES = [
|
|
16
|
+
"catalog:read",
|
|
17
|
+
"balance:read",
|
|
18
|
+
"usage:read",
|
|
19
|
+
"models:invoke"
|
|
20
|
+
];
|
|
21
|
+
const MCP_SCOPES = ["mcp:bounties", "mcp:drawing"];
|
|
22
|
+
const INITIAL_SCOPES = [...APPLICATION_SCOPES, ...MCP_SCOPES];
|
|
23
|
+
const SUPPORTED_APIS = [
|
|
24
|
+
"openai-completions",
|
|
25
|
+
"openai-responses",
|
|
26
|
+
"anthropic-messages"
|
|
27
|
+
];
|
|
28
|
+
/** Messages are deliberately fixed: never include an HTTP body, code, or credential. */
|
|
29
|
+
var LmmError = class extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(code, message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "LmmError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function requireValue(condition, message = "Invalid LMM protocol response.") {
|
|
38
|
+
if (!condition) throw new LmmError("invalid_response", message);
|
|
39
|
+
}
|
|
40
|
+
function object(value) {
|
|
41
|
+
requireValue(typeof value === "object" && value !== null && !Array.isArray(value));
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function text(value, max = 1024) {
|
|
45
|
+
requireValue(typeof value === "string" && value.length > 0 && value.length <= max && !/[\p{Cc}\p{Cf}]/u.test(value));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function finite(value) {
|
|
49
|
+
requireValue(typeof value === "number" && Number.isFinite(value));
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function nonnegative(value) {
|
|
53
|
+
const result = finite(value);
|
|
54
|
+
requireValue(result >= 0);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function nullableNumber(value) {
|
|
58
|
+
return value === null ? null : nonnegative(value);
|
|
59
|
+
}
|
|
60
|
+
function unixSeconds(value) {
|
|
61
|
+
const result = nonnegative(value);
|
|
62
|
+
requireValue(Number.isSafeInteger(result));
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function base64url(value) {
|
|
66
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
67
|
+
}
|
|
68
|
+
function canonicalId(value) {
|
|
69
|
+
const id = text(value, 4096);
|
|
70
|
+
requireValue(/^[A-Za-z0-9_-]+$/.test(id));
|
|
71
|
+
const decoded = Buffer.from(id, "base64url").toString("utf8");
|
|
72
|
+
requireValue(base64url(decoded) === id);
|
|
73
|
+
text(decoded);
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
function parseScope(value) {
|
|
77
|
+
const scope = text(value, 16384);
|
|
78
|
+
const parts = scope.split(" ");
|
|
79
|
+
requireValue(new Set(parts).size === parts.length);
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (INITIAL_SCOPES.includes(part)) continue;
|
|
82
|
+
requireValue(part.startsWith("group:"));
|
|
83
|
+
canonicalId(part.slice(6));
|
|
84
|
+
}
|
|
85
|
+
return scope;
|
|
86
|
+
}
|
|
87
|
+
function accessToken(value) {
|
|
88
|
+
const token = text(value, 4096);
|
|
89
|
+
requireValue(/^lmm_at_[A-Za-z0-9_-]+$/.test(token), "LMM requires its OAuth access token, not an API key.");
|
|
90
|
+
return token;
|
|
91
|
+
}
|
|
92
|
+
function credential(value, issuer) {
|
|
93
|
+
const item = object(value);
|
|
94
|
+
requireValue(item.type === "oauth" && item.lmm_issuer === issuer && item.lmm_resource === `${issuer}/api/oauth2`, "LMM credential belongs to another issuer or is not OAuth. Use /login.");
|
|
95
|
+
accessToken(item.access);
|
|
96
|
+
text(item.refresh, 4096);
|
|
97
|
+
text(item.lmm_session, 128);
|
|
98
|
+
parseScope(item.scope);
|
|
99
|
+
nonnegative(item.expires);
|
|
100
|
+
return item;
|
|
101
|
+
}
|
|
102
|
+
function boundedSignal(signal, timeoutMs = 15e3) {
|
|
103
|
+
return AbortSignal.any([...signal ? [signal] : [], AbortSignal.timeout(timeoutMs)]);
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region vendor/pi-lmm-provider/src/balance.ts
|
|
107
|
+
function parseBalance(value) {
|
|
108
|
+
const row = object(value);
|
|
109
|
+
requireValue(row.schema_version === 1 && (row.currency === "platform_credit" || row.currency === "USD") && row.authorization_limit === null);
|
|
110
|
+
const quota = finite(row.quota);
|
|
111
|
+
const quotaPerUnit = finite(row.quota_per_unit);
|
|
112
|
+
requireValue(Number.isSafeInteger(quota) && quotaPerUnit > 0);
|
|
113
|
+
const remaining = row.balance === null ? null : finite(row.balance);
|
|
114
|
+
if (remaining !== null) {
|
|
115
|
+
const expected = quota / quotaPerUnit;
|
|
116
|
+
requireValue(Math.abs(remaining - expected) <= Math.max(1e-9, Math.abs(expected) * 1e-12), "LMM wallet amounts are inconsistent.");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
currency: "platform_credit",
|
|
120
|
+
remaining,
|
|
121
|
+
quota,
|
|
122
|
+
quota_per_unit: quotaPerUnit,
|
|
123
|
+
updated_at: unixSeconds(row.updated_at),
|
|
124
|
+
authorization_limit: null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function balanceStatus(balance, stale = false) {
|
|
128
|
+
if (!balance || balance.remaining === null) return "LMM · balance unavailable";
|
|
129
|
+
return `LMM ${new Intl.NumberFormat("en-US", { maximumFractionDigits: 6 }).format(balance.remaining)} platform credits${stale ? " · stale" : ""} · wallet only`;
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region vendor/pi-lmm-provider/src/catalog.ts
|
|
133
|
+
function parseGroup(value) {
|
|
134
|
+
const row = object(value);
|
|
135
|
+
const id = canonicalId(row.id);
|
|
136
|
+
const name = text(row.name);
|
|
137
|
+
requireValue(id === base64url(name) && row.scope === `group:${id}`);
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
name,
|
|
141
|
+
scope: `group:${id}`,
|
|
142
|
+
multiplier: nullableNumber(row.multiplier)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parsePricing(value) {
|
|
146
|
+
const row = object(value);
|
|
147
|
+
requireValue(typeof row.final_cost_depends_on_usage === "boolean");
|
|
148
|
+
return {
|
|
149
|
+
currency: text(row.currency, 16),
|
|
150
|
+
unit: text(row.unit, 64),
|
|
151
|
+
price_basis: text(row.price_basis, 64),
|
|
152
|
+
group_multiplier: nullableNumber(row.group_multiplier),
|
|
153
|
+
trust_multiplier: nullableNumber(row.trust_multiplier),
|
|
154
|
+
input: nullableNumber(row.input),
|
|
155
|
+
output: nullableNumber(row.output),
|
|
156
|
+
cache_read: nullableNumber(row.cache_read),
|
|
157
|
+
cache_write: nullableNumber(row.cache_write),
|
|
158
|
+
request: nullableNumber(row.request),
|
|
159
|
+
final_cost_depends_on_usage: row.final_cost_depends_on_usage,
|
|
160
|
+
updated_at: unixSeconds(row.updated_at)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function nativeCost(value, pricing) {
|
|
164
|
+
if (value === null || value === void 0) return null;
|
|
165
|
+
const row = object(value);
|
|
166
|
+
const cost = {
|
|
167
|
+
input: nonnegative(row.input),
|
|
168
|
+
output: nonnegative(row.output),
|
|
169
|
+
cacheRead: nonnegative(row.cacheRead),
|
|
170
|
+
cacheWrite: nonnegative(row.cacheWrite)
|
|
171
|
+
};
|
|
172
|
+
requireValue(pricing.currency === "USD" && pricing.unit === "million_tokens" && (pricing.price_basis === "configured_base_rates" || pricing.price_basis === "dynamic_estimate") && pricing.request === null, "LMM advertised a native cost for incompatible billing.");
|
|
173
|
+
requireValue(cost.input === pricing.input && cost.output === pricing.output && cost.cacheRead === pricing.cache_read && cost.cacheWrite === pricing.cache_write, "LMM native cost differs from its already-adjusted server rates.");
|
|
174
|
+
return cost;
|
|
175
|
+
}
|
|
176
|
+
function parseEntry(value, groups) {
|
|
177
|
+
const row = object(value);
|
|
178
|
+
const group_id = canonicalId(row.group_id);
|
|
179
|
+
const group = text(row.group);
|
|
180
|
+
const upstream_model = text(row.upstream_model, 512);
|
|
181
|
+
const id = text(row.id, 4096);
|
|
182
|
+
requireValue(groups.get(group_id)?.name === group && id === `lmm:${group_id}:${base64url(upstream_model)}`);
|
|
183
|
+
requireValue(Array.isArray(row.apis) && row.apis.length > 0 && row.apis.length <= 16);
|
|
184
|
+
const apis = [];
|
|
185
|
+
for (const value of row.apis) {
|
|
186
|
+
const api = text(value, 64);
|
|
187
|
+
if (SUPPORTED_APIS.includes(api)) apis.push(api);
|
|
188
|
+
}
|
|
189
|
+
requireValue(new Set(apis).size === apis.length);
|
|
190
|
+
const pricing = parsePricing(row.pricing);
|
|
191
|
+
return {
|
|
192
|
+
id,
|
|
193
|
+
group_id,
|
|
194
|
+
group,
|
|
195
|
+
upstream_model,
|
|
196
|
+
name: text(row.name),
|
|
197
|
+
apis,
|
|
198
|
+
pricing,
|
|
199
|
+
native_cost: nativeCost(row.native_cost, pricing)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function parseCatalog(value, resource, grantedScope) {
|
|
203
|
+
const body = object(value);
|
|
204
|
+
requireValue(body.schema_version === 1 && body.resource === resource);
|
|
205
|
+
requireValue(Array.isArray(body.groups) && body.groups.length <= 2e3 && Array.isArray(body.models) && body.models.length <= 2e4);
|
|
206
|
+
const scopes = new Set(grantedScope.split(" "));
|
|
207
|
+
const groups = body.groups.map(parseGroup);
|
|
208
|
+
const byGroup = new Map(groups.map((group) => [group.id, group]));
|
|
209
|
+
requireValue(byGroup.size === groups.length && groups.every((group) => scopes.has(group.scope)), "LMM catalog contains a group outside the granted OAuth scopes.");
|
|
210
|
+
const models = body.models.map((value) => parseEntry(value, byGroup));
|
|
211
|
+
requireValue(new Set(models.map((model) => model.id)).size === models.length);
|
|
212
|
+
return {
|
|
213
|
+
schema_version: 1,
|
|
214
|
+
resource,
|
|
215
|
+
updated_at: unixSeconds(body.updated_at),
|
|
216
|
+
groups,
|
|
217
|
+
models
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** Pi renders model.id in its native picker. Keep wire IDs separate from readable IDs. */
|
|
221
|
+
function nativeModelId(entry) {
|
|
222
|
+
const part = (value) => value.replaceAll("%", "%25").replaceAll(" / ", "%20%2F%20");
|
|
223
|
+
return `${part(entry.group)} / ${part(entry.upstream_model)}`;
|
|
224
|
+
}
|
|
225
|
+
function admitCatalog(catalog, issuer, resolve = () => void 0) {
|
|
226
|
+
return catalog.models.map((entry) => {
|
|
227
|
+
const capabilities = resolve(entry);
|
|
228
|
+
if (!capabilities) return {
|
|
229
|
+
entry,
|
|
230
|
+
reason: "Capability metadata unavailable; not registered in /model."
|
|
231
|
+
};
|
|
232
|
+
const cost = entry.native_cost ?? capabilities.referenceCost;
|
|
233
|
+
if (!cost) return {
|
|
234
|
+
entry,
|
|
235
|
+
reason: "No native or reference cost; available for price inspection only."
|
|
236
|
+
};
|
|
237
|
+
requireValue(entry.apis.includes(capabilities.api) && Number.isSafeInteger(capabilities.contextWindow) && capabilities.contextWindow > 0 && Number.isSafeInteger(capabilities.maxTokens) && capabilities.maxTokens > 0 && capabilities.maxTokens <= capabilities.contextWindow && typeof capabilities.reasoning === "boolean" && capabilities.input.length > 0 && capabilities.input.every((input) => input === "text" || input === "image"));
|
|
238
|
+
text(capabilities.provenance);
|
|
239
|
+
const multiplier = entry.pricing.group_multiplier === null ? "unknown" : `${entry.pricing.group_multiplier}×`;
|
|
240
|
+
const estimate = entry.pricing.price_basis === "dynamic_estimate" ? " · estimate" : "";
|
|
241
|
+
const variable = entry.native_cost ? "" : " · LMM variable billing";
|
|
242
|
+
return {
|
|
243
|
+
entry,
|
|
244
|
+
model: {
|
|
245
|
+
id: nativeModelId(entry),
|
|
246
|
+
name: `${entry.group} / ${entry.upstream_model} · group ${multiplier}${estimate}${variable}`,
|
|
247
|
+
provider: "lmm",
|
|
248
|
+
api: capabilities.api,
|
|
249
|
+
baseUrl: capabilities.api === "anthropic-messages" ? issuer : `${issuer}/v1`,
|
|
250
|
+
reasoning: capabilities.reasoning,
|
|
251
|
+
input: [...capabilities.input],
|
|
252
|
+
contextWindow: capabilities.contextWindow,
|
|
253
|
+
maxTokens: capabilities.maxTokens,
|
|
254
|
+
compat: structuredClone(capabilities.compat),
|
|
255
|
+
cost: structuredClone(cost),
|
|
256
|
+
...capabilities.thinkingLevelMap ? { thinkingLevelMap: structuredClone(capabilities.thinkingLevelMap) } : {}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function amount(value) {
|
|
262
|
+
return value === null ? "unknown" : String(value);
|
|
263
|
+
}
|
|
264
|
+
function priceReport(admissions, filter = "") {
|
|
265
|
+
const selected = admissions.filter(({ entry }) => `${entry.id} ${entry.name}`.toLowerCase().includes(filter.toLowerCase()));
|
|
266
|
+
const lines = selected.slice(0, 40).map(({ entry, reason }) => {
|
|
267
|
+
const p = entry.pricing;
|
|
268
|
+
return [
|
|
269
|
+
`${entry.group} / ${entry.upstream_model}`,
|
|
270
|
+
` ${p.currency}/${p.unit}; ${p.price_basis}; group ×${amount(p.group_multiplier)}, trust ×${amount(p.trust_multiplier)} (already included)`,
|
|
271
|
+
` input ${amount(p.input)}, output ${amount(p.output)}, cache read ${amount(p.cache_read)}, cache write ${amount(p.cache_write)}, request ${amount(p.request)}`,
|
|
272
|
+
` ${reason ?? "Select using native /model."} Snapshot ${(/* @__PURE__ */ new Date(p.updated_at * 1e3)).toISOString()}.`
|
|
273
|
+
].join("\n");
|
|
274
|
+
});
|
|
275
|
+
if (selected.length > 40) lines.push(`Showing 40 of ${selected.length}; pass an ID or name to /lmm-prices to narrow the read-only report.`);
|
|
276
|
+
lines.push("Final billing depends on usage and current server policy; this is not a locked quote or a budget approval. Unknown is not free.");
|
|
277
|
+
return lines.join("\n");
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region vendor/pi-lmm-provider/src/capabilities.ts
|
|
281
|
+
const LANGUAGE_APIS = [
|
|
282
|
+
"anthropic-messages",
|
|
283
|
+
"azure-openai-responses",
|
|
284
|
+
"bedrock-converse-stream",
|
|
285
|
+
"google-generative-ai",
|
|
286
|
+
"google-vertex",
|
|
287
|
+
"mistral-conversations",
|
|
288
|
+
"openai-codex-responses",
|
|
289
|
+
"openai-completions",
|
|
290
|
+
"openai-responses"
|
|
291
|
+
];
|
|
292
|
+
const preferredSources = [
|
|
293
|
+
"openai",
|
|
294
|
+
"anthropic",
|
|
295
|
+
"deepseek",
|
|
296
|
+
"xai",
|
|
297
|
+
"moonshotai",
|
|
298
|
+
"minimax",
|
|
299
|
+
"google",
|
|
300
|
+
"zai"
|
|
301
|
+
].map((provider) => [provider, new Map(getBuiltinModels(provider).map((model) => [model.id, model]))]);
|
|
302
|
+
const preferredProviders = new Set(preferredSources.map(([provider]) => provider));
|
|
303
|
+
const fallbackSources = getBuiltinProviders().filter((provider) => !preferredProviders.has(provider)).map((provider) => [provider, new Map(getBuiltinModels(provider).map((model) => [model.id, model]))]);
|
|
304
|
+
function gatewayCompat(entry, compat) {
|
|
305
|
+
if (entry.group === "国产[Kimi/Deepseek/GLM]" && (entry.upstream_model === "glm-5.3" || entry.upstream_model === "glm-5.3-flash")) return {
|
|
306
|
+
...compat,
|
|
307
|
+
supportsLongCacheRetention: true,
|
|
308
|
+
sendSessionAffinityHeaders: true,
|
|
309
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
310
|
+
thinkingFormat: "deepseek"
|
|
311
|
+
};
|
|
312
|
+
return compat;
|
|
313
|
+
}
|
|
314
|
+
function candidates(entry, sources) {
|
|
315
|
+
const result = [];
|
|
316
|
+
for (const [provider, models] of sources) {
|
|
317
|
+
const model = models.get(entry.upstream_model);
|
|
318
|
+
if (!model) continue;
|
|
319
|
+
const nativeProtocol = SUPPORTED_APIS.includes(model.api) && entry.apis.includes(model.api);
|
|
320
|
+
if (!nativeProtocol && (!entry.apis.includes("openai-completions") || !LANGUAGE_APIS.includes(model.api))) continue;
|
|
321
|
+
const compat = gatewayCompat(entry, nativeProtocol ? structuredClone(model.compat ?? {}) : {
|
|
322
|
+
supportsDeveloperRole: false,
|
|
323
|
+
supportsStore: false,
|
|
324
|
+
supportsReasoningEffort: false,
|
|
325
|
+
maxTokensField: provider === "openai" ? "max_completion_tokens" : "max_tokens"
|
|
326
|
+
});
|
|
327
|
+
if ("allowedFallbackModels" in compat) delete compat.allowedFallbackModels;
|
|
328
|
+
result.push({
|
|
329
|
+
api: nativeProtocol ? model.api : "openai-completions",
|
|
330
|
+
contextWindow: model.contextWindow,
|
|
331
|
+
maxTokens: model.maxTokens,
|
|
332
|
+
reasoning: nativeProtocol && model.reasoning,
|
|
333
|
+
input: [...model.input],
|
|
334
|
+
compat,
|
|
335
|
+
referenceCost: structuredClone(model.cost),
|
|
336
|
+
thinkingLevelMap: nativeProtocol && model.thinkingLevelMap ? structuredClone(model.thinkingLevelMap) : void 0,
|
|
337
|
+
provenance: `pi-ai vendor catalog: ${provider}/${model.id}${nativeProtocol ? "" : "; server-advertised OpenAI-compatible transport"}`
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
/** Exact vendor catalog matches only. LMM account pricing always comes from its server. */
|
|
343
|
+
const resolveKnownCapabilities = (entry) => {
|
|
344
|
+
const matches = candidates(entry, preferredSources);
|
|
345
|
+
const resolved = matches.length ? matches : candidates(entry, fallbackSources);
|
|
346
|
+
if (!resolved.length) return void 0;
|
|
347
|
+
const profile = ({ provenance: _source, referenceCost: _cost, ...rest }) => JSON.stringify(rest);
|
|
348
|
+
const first = resolved[0];
|
|
349
|
+
return resolved.every((candidate) => profile(candidate) === profile(first)) ? first : void 0;
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region vendor/pi-lmm-provider/src/http.ts
|
|
353
|
+
var LmmHttp = class {
|
|
354
|
+
issuer;
|
|
355
|
+
resource;
|
|
356
|
+
fetch;
|
|
357
|
+
timeoutMs;
|
|
358
|
+
constructor(options = {}) {
|
|
359
|
+
const raw = options.issuer ?? "https://api.lmm.best";
|
|
360
|
+
const url = new URL(raw);
|
|
361
|
+
requireValue(!url.username && !url.password && !url.search && !url.hash && url.pathname === "/", "LMM issuer must be a trusted HTTPS origin without a path or credentials.");
|
|
362
|
+
requireValue(url.protocol === "https:" || options.allowLoopbackHttpForTests === true && url.protocol === "http:" && url.hostname === "127.0.0.1", "LMM requires HTTPS (except explicit local test fixtures).");
|
|
363
|
+
this.issuer = url.origin;
|
|
364
|
+
this.resource = `${this.issuer}/api/oauth2`;
|
|
365
|
+
this.fetch = options.fetch ?? globalThis.fetch;
|
|
366
|
+
this.timeoutMs = options.timeoutMs ?? 15e3;
|
|
367
|
+
}
|
|
368
|
+
async request(path, init = {}, maxBytes = 2e6) {
|
|
369
|
+
const signal = boundedSignal(init.signal ?? void 0, this.timeoutMs);
|
|
370
|
+
try {
|
|
371
|
+
const response = await this.fetch(`${this.issuer}${path}`, {
|
|
372
|
+
...init,
|
|
373
|
+
signal,
|
|
374
|
+
redirect: "error",
|
|
375
|
+
credentials: "omit",
|
|
376
|
+
cache: "no-store",
|
|
377
|
+
headers: {
|
|
378
|
+
accept: "application/json",
|
|
379
|
+
...init.headers
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
let errorCode = "";
|
|
384
|
+
if (response.headers.get("content-type")?.split(";")[0]?.trim() === "application/json" && response.body) try {
|
|
385
|
+
const body = object(JSON.parse(await response.text()));
|
|
386
|
+
if (typeof body.error === "object" && body.error !== null && !Array.isArray(body.error) && typeof body.error.code === "string") errorCode = body.error.code;
|
|
387
|
+
else if (typeof body.code === "string") errorCode = body.code;
|
|
388
|
+
} catch {}
|
|
389
|
+
else await response.body?.cancel();
|
|
390
|
+
if (errorCode === "IP_ACCESS_ROUTE_REJECTED") throw new LmmError("ip_policy", "LMM request was blocked by the current IP access policy. Change network or ask the administrator to allow this IP.");
|
|
391
|
+
if (response.status === 401 || response.status === 403) throw new LmmError("unauthorized", "LMM authorization is unavailable or was revoked. Use /login; no API-key fallback is allowed.");
|
|
392
|
+
if (response.status === 429) throw new LmmError("rate_limited", "LMM rate limit reached. Wait a moment and retry.");
|
|
393
|
+
if (response.status >= 500) throw new LmmError("upstream_unavailable", `LMM service is temporarily unavailable (HTTP ${response.status}). Try again shortly.`);
|
|
394
|
+
throw new LmmError("http_error", `LMM HTTP request failed (${response.status}).`);
|
|
395
|
+
}
|
|
396
|
+
if (response.status === 204 || path === "/api/oauth2/revoke") {
|
|
397
|
+
await response.body?.cancel();
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
requireValue(response.headers.get("content-type")?.split(";")[0]?.trim() === "application/json");
|
|
401
|
+
requireValue(response.body);
|
|
402
|
+
const reader = response.body.getReader();
|
|
403
|
+
const chunks = [];
|
|
404
|
+
let bytes = 0;
|
|
405
|
+
try {
|
|
406
|
+
while (true) {
|
|
407
|
+
signal.throwIfAborted();
|
|
408
|
+
const { value, done } = await reader.read();
|
|
409
|
+
if (done) break;
|
|
410
|
+
bytes += value.byteLength;
|
|
411
|
+
requireValue(bytes <= maxBytes, "LMM response exceeded its size limit.");
|
|
412
|
+
chunks.push(value);
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
await reader.cancel().catch(() => {});
|
|
416
|
+
reader.releaseLock();
|
|
417
|
+
}
|
|
418
|
+
return object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error instanceof LmmError) throw error;
|
|
421
|
+
if (signal.aborted) throw new LmmError("aborted", "LMM request cancelled or timed out.");
|
|
422
|
+
throw new LmmError("transport_error", "LMM network request failed. Check your connection and retry.");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
form(path, fields, signal) {
|
|
426
|
+
return this.request(path, {
|
|
427
|
+
method: "POST",
|
|
428
|
+
signal,
|
|
429
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
430
|
+
body: new URLSearchParams(fields)
|
|
431
|
+
}, 64e3);
|
|
432
|
+
}
|
|
433
|
+
bearer(path, access, signal) {
|
|
434
|
+
return this.request(path, {
|
|
435
|
+
headers: { authorization: `Bearer ${access}` },
|
|
436
|
+
signal
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region vendor/pi-lmm-provider/src/callback.ts
|
|
442
|
+
function equal(left, right) {
|
|
443
|
+
if (left === null || left.length > 4096) return false;
|
|
444
|
+
const a = Buffer.from(left);
|
|
445
|
+
const b = Buffer.from(right);
|
|
446
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
447
|
+
}
|
|
448
|
+
function reply(response, status, message) {
|
|
449
|
+
response.writeHead(status, {
|
|
450
|
+
"content-type": "text/plain; charset=utf-8",
|
|
451
|
+
"cache-control": "no-store",
|
|
452
|
+
"referrer-policy": "no-referrer",
|
|
453
|
+
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
|
|
454
|
+
"x-content-type-options": "nosniff",
|
|
455
|
+
connection: "close"
|
|
456
|
+
});
|
|
457
|
+
response.end(message);
|
|
458
|
+
}
|
|
459
|
+
/** Bind before returning a redirect URI. Invalid/error callbacks never settle the login. */
|
|
460
|
+
async function listenCallback(issuer, state, signal, hostName = "Pi") {
|
|
461
|
+
signal.throwIfAborted();
|
|
462
|
+
let accept = () => {};
|
|
463
|
+
let reject = () => {};
|
|
464
|
+
const code = new Promise((resolve, fail) => {
|
|
465
|
+
accept = resolve;
|
|
466
|
+
reject = fail;
|
|
467
|
+
});
|
|
468
|
+
code.catch(() => {});
|
|
469
|
+
let authority = "";
|
|
470
|
+
let settled = false;
|
|
471
|
+
let closed = false;
|
|
472
|
+
const server = createServer({ maxHeaderSize: 8192 }, (request, response) => {
|
|
473
|
+
if (settled || request.method !== "GET" || request.headers.host !== authority || request.socket.remoteAddress !== "127.0.0.1" || request.url?.split("?")[0] !== "/oauth/lmm/callback") {
|
|
474
|
+
reply(response, 400, `Invalid OAuth callback. Return to ${hostName} to cancel or retry.`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const params = new URL(request.url, `http://${authority}`).searchParams;
|
|
478
|
+
const valid = [
|
|
479
|
+
"state",
|
|
480
|
+
"iss",
|
|
481
|
+
"code"
|
|
482
|
+
].every((key) => params.getAll(key).length === 1) && [...params.keys()].every((key) => [
|
|
483
|
+
"state",
|
|
484
|
+
"iss",
|
|
485
|
+
"code"
|
|
486
|
+
].includes(key)) && equal(params.get("state"), state) && params.get("iss") === issuer;
|
|
487
|
+
const returnedCode = params.get("code");
|
|
488
|
+
const errorKeys = [
|
|
489
|
+
"state",
|
|
490
|
+
"iss",
|
|
491
|
+
"error"
|
|
492
|
+
];
|
|
493
|
+
const hasError = params.has("error");
|
|
494
|
+
const errorValid = errorKeys.every((key) => params.getAll(key).length === 1) && [...params.keys()].every((key) => errorKeys.includes(key) || key === "error_description" || key === "error_uri") && params.getAll("error_description").length <= 1 && params.getAll("error_uri").length <= 1 && equal(params.get("state"), state) && params.get("iss") === issuer && /^[A-Za-z0-9._~-]{1,256}$/.test(params.get("error") ?? "");
|
|
495
|
+
if (hasError && errorValid && !params.has("code")) {
|
|
496
|
+
settled = true;
|
|
497
|
+
reply(response, 200, `Authorization was not granted. Return to ${hostName}.`);
|
|
498
|
+
reject(new LmmError("oauth_denied", "LMM authorization was denied."));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!valid || returnedCode === null || !/^[A-Za-z0-9._~-]{1,4096}$/.test(returnedCode)) {
|
|
502
|
+
reply(response, 400, `Unverified OAuth callback. The login is still waiting in ${hostName}.`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
settled = true;
|
|
506
|
+
reply(response, 200, `Authorization received. Return to ${hostName} to finish signing in.`);
|
|
507
|
+
accept(returnedCode);
|
|
508
|
+
});
|
|
509
|
+
server.requestTimeout = 5e3;
|
|
510
|
+
server.headersTimeout = 5e3;
|
|
511
|
+
server.keepAliveTimeout = 1;
|
|
512
|
+
const close = () => {
|
|
513
|
+
if (closed) return;
|
|
514
|
+
closed = true;
|
|
515
|
+
signal.removeEventListener("abort", abort);
|
|
516
|
+
server.close();
|
|
517
|
+
server.closeAllConnections();
|
|
518
|
+
if (!settled) {
|
|
519
|
+
settled = true;
|
|
520
|
+
reject(new LmmError("aborted", "LMM login cancelled or timed out."));
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const abort = () => close();
|
|
524
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
525
|
+
try {
|
|
526
|
+
await new Promise((resolve, fail) => {
|
|
527
|
+
const onError = () => fail(new LmmError("callback_unavailable", "Could not listen on the local OAuth callback address."));
|
|
528
|
+
server.once("error", onError);
|
|
529
|
+
server.listen(0, "127.0.0.1", () => {
|
|
530
|
+
server.removeListener("error", onError);
|
|
531
|
+
resolve();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
if (signal.aborted) {
|
|
535
|
+
server.close();
|
|
536
|
+
server.closeAllConnections();
|
|
537
|
+
throw new LmmError("aborted", "LMM login cancelled or timed out.");
|
|
538
|
+
}
|
|
539
|
+
server.on("error", () => {
|
|
540
|
+
reject(new LmmError("callback_unavailable", "The local OAuth callback listener failed."));
|
|
541
|
+
close();
|
|
542
|
+
});
|
|
543
|
+
const address = server.address();
|
|
544
|
+
if (!address || typeof address === "string") throw new LmmError("callback_unavailable", "No local OAuth callback port was allocated.");
|
|
545
|
+
authority = `127.0.0.1:${address.port}`;
|
|
546
|
+
return {
|
|
547
|
+
redirectUri: `http://${authority}${CALLBACK_PATH}`,
|
|
548
|
+
code,
|
|
549
|
+
close
|
|
550
|
+
};
|
|
551
|
+
} catch (error) {
|
|
552
|
+
close();
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region vendor/pi-lmm-provider/src/refresh-journal.ts
|
|
558
|
+
const FIXED_FAILURE = "LMM refresh could not be safely recorded. Use /login.";
|
|
559
|
+
const ALREADY_ATTEMPTED = "LMM refresh was already attempted. Use /login.";
|
|
560
|
+
function refreshDigest(refresh) {
|
|
561
|
+
return createHash("sha256").update(refresh, "utf8").digest("hex");
|
|
562
|
+
}
|
|
563
|
+
/** Durable, credential-free fencing for one-shot refresh tokens. */
|
|
564
|
+
var RefreshJournal = class {
|
|
565
|
+
directory;
|
|
566
|
+
constructor(directory) {
|
|
567
|
+
if (!directory) throw new LmmError("refresh_storage_unverified", FIXED_FAILURE);
|
|
568
|
+
this.directory = directory;
|
|
569
|
+
}
|
|
570
|
+
async begin(issuer, refresh) {
|
|
571
|
+
const digest = refreshDigest(refresh);
|
|
572
|
+
const marker = join(this.directory, `${refreshDigest(`${issuer}\n${digest}`)}.refresh`);
|
|
573
|
+
let handle;
|
|
574
|
+
try {
|
|
575
|
+
const firstCreated = await mkdir(this.directory, {
|
|
576
|
+
recursive: true,
|
|
577
|
+
mode: 448
|
|
578
|
+
});
|
|
579
|
+
handle = await open(marker, "wx", 384);
|
|
580
|
+
await handle.writeFile(JSON.stringify({
|
|
581
|
+
issuer,
|
|
582
|
+
refresh_sha256: digest
|
|
583
|
+
}) + "\n", "utf8");
|
|
584
|
+
await handle.sync();
|
|
585
|
+
await handle.close();
|
|
586
|
+
handle = void 0;
|
|
587
|
+
const directoryHandle = await open(this.directory, "r");
|
|
588
|
+
try {
|
|
589
|
+
await directoryHandle.sync();
|
|
590
|
+
} finally {
|
|
591
|
+
await directoryHandle.close();
|
|
592
|
+
}
|
|
593
|
+
if (firstCreated) {
|
|
594
|
+
const firstCreatedParent = dirname(resolve(firstCreated));
|
|
595
|
+
let parent = dirname(resolve(this.directory));
|
|
596
|
+
while (true) {
|
|
597
|
+
const parentHandle = await open(parent, "r");
|
|
598
|
+
try {
|
|
599
|
+
await parentHandle.sync();
|
|
600
|
+
} finally {
|
|
601
|
+
await parentHandle.close();
|
|
602
|
+
}
|
|
603
|
+
if (parent === firstCreatedParent || parent === dirname(parent)) break;
|
|
604
|
+
parent = dirname(parent);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
await handle?.close().catch(() => {});
|
|
609
|
+
if (error.code === "EEXIST") throw new LmmError("refresh_already_attempted", ALREADY_ATTEMPTED);
|
|
610
|
+
throw new LmmError("refresh_storage_unavailable", FIXED_FAILURE);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region vendor/pi-lmm-provider/src/oauth.ts
|
|
616
|
+
const REFRESH_BLOCKED = "Automatic LMM refresh requires a configured durable refresh journal. Use /login.";
|
|
617
|
+
var LmmOAuth = class {
|
|
618
|
+
http;
|
|
619
|
+
loginTimeoutMs;
|
|
620
|
+
refreshJournal;
|
|
621
|
+
clientId;
|
|
622
|
+
hostName;
|
|
623
|
+
constructor(http, loginTimeoutMs = 18e4, refreshJournalDirectory, clientId = CLIENT_ID, hostName = "Pi") {
|
|
624
|
+
this.http = http;
|
|
625
|
+
this.loginTimeoutMs = loginTimeoutMs;
|
|
626
|
+
this.refreshJournal = refreshJournalDirectory === void 0 ? void 0 : new RefreshJournal(refreshJournalDirectory);
|
|
627
|
+
requireValue(/^[a-z0-9][a-z0-9-]{0,63}$/.test(clientId), "Invalid LMM OAuth client identifier.");
|
|
628
|
+
requireValue(/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(hostName), "Invalid LMM OAuth host name.");
|
|
629
|
+
this.clientId = clientId;
|
|
630
|
+
this.hostName = hostName;
|
|
631
|
+
}
|
|
632
|
+
async discover(signal) {
|
|
633
|
+
const [authorization, resource] = await Promise.all([this.http.request("/.well-known/oauth-authorization-server", { signal }, 64e3), this.http.request("/.well-known/oauth-protected-resource/api/oauth2", { signal }, 64e3)]);
|
|
634
|
+
requireValue(authorization.issuer === this.http.issuer && authorization.authorization_endpoint === `${this.http.resource}/authorize` && authorization.token_endpoint === `${this.http.resource}/token` && authorization.revocation_endpoint === `${this.http.resource}/revoke`, "LMM OAuth discovery issuer or endpoint does not match the configured issuer.");
|
|
635
|
+
requireValue(Array.isArray(authorization.code_challenge_methods_supported) && authorization.code_challenge_methods_supported.includes("S256"), "LMM OAuth discovery does not support the required S256 PKCE method.");
|
|
636
|
+
requireValue(Array.isArray(authorization.response_types_supported) && authorization.response_types_supported.includes("code"), "LMM OAuth discovery does not support the authorization-code flow.");
|
|
637
|
+
requireValue(authorization.authorization_response_iss_parameter_supported === true, "LMM discovery must advertise issuer-bound authorization responses.");
|
|
638
|
+
requireValue(resource.resource === this.http.resource && Array.isArray(resource.authorization_servers) && resource.authorization_servers.length === 1 && resource.authorization_servers[0] === this.http.issuer, "LMM OAuth protected-resource metadata does not match the configured resource.");
|
|
639
|
+
}
|
|
640
|
+
async login(interaction) {
|
|
641
|
+
const signal = boundedSignal(interaction.signal, this.loginTimeoutMs);
|
|
642
|
+
let callback;
|
|
643
|
+
try {
|
|
644
|
+
await this.discover(signal);
|
|
645
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
646
|
+
const state = randomBytes(32).toString("base64url");
|
|
647
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
648
|
+
callback = await listenCallback(this.http.issuer, state, signal, this.hostName);
|
|
649
|
+
const url = new URL(`${this.http.resource}/authorize`);
|
|
650
|
+
url.search = new URLSearchParams({
|
|
651
|
+
client_id: this.clientId,
|
|
652
|
+
response_type: "code",
|
|
653
|
+
redirect_uri: callback.redirectUri,
|
|
654
|
+
scope: INITIAL_SCOPES.join(" "),
|
|
655
|
+
resource: this.http.resource,
|
|
656
|
+
code_challenge: challenge,
|
|
657
|
+
code_challenge_method: "S256",
|
|
658
|
+
state
|
|
659
|
+
}).toString();
|
|
660
|
+
interaction.notify({
|
|
661
|
+
type: "auth_url",
|
|
662
|
+
url: url.href,
|
|
663
|
+
instructions: `Sign in and approve LMM in your browser, then return to ${this.hostName}. Cancel in ${this.hostName} to stop waiting.`
|
|
664
|
+
});
|
|
665
|
+
const code = await callback.code;
|
|
666
|
+
const redirectUri = callback.redirectUri;
|
|
667
|
+
callback.close();
|
|
668
|
+
callback = void 0;
|
|
669
|
+
signal.throwIfAborted();
|
|
670
|
+
const startedAt = Date.now();
|
|
671
|
+
const response = await this.http.form("/api/oauth2/token", {
|
|
672
|
+
grant_type: "authorization_code",
|
|
673
|
+
client_id: this.clientId,
|
|
674
|
+
code,
|
|
675
|
+
redirect_uri: redirectUri,
|
|
676
|
+
code_verifier: verifier,
|
|
677
|
+
resource: this.http.resource
|
|
678
|
+
}, signal);
|
|
679
|
+
return this.parseToken(response, startedAt);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (signal.aborted || error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) throw new LmmError("aborted", "LMM login cancelled or timed out. Start /login again.");
|
|
682
|
+
throw error;
|
|
683
|
+
} finally {
|
|
684
|
+
callback?.close();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
parseToken(response, startedAt, previous) {
|
|
688
|
+
requireValue(response.token_type === "Bearer", "LMM OAuth token response did not contain Bearer token type.");
|
|
689
|
+
const access = accessToken(response.access_token);
|
|
690
|
+
const refresh = text(response.refresh_token, 4096);
|
|
691
|
+
requireValue(!/\s/.test(refresh) && refresh !== access, "LMM OAuth token response contained an invalid refresh token.");
|
|
692
|
+
const lifetime = nonnegative(response.expires_in);
|
|
693
|
+
requireValue(Number.isSafeInteger(lifetime) && lifetime > 0 && lifetime <= 86400, "LMM OAuth token response contained an invalid expiration.");
|
|
694
|
+
const scope = parseScope(response.scope === void 0 ? previous?.scope : response.scope);
|
|
695
|
+
const scopes = new Set(scope.split(" "));
|
|
696
|
+
if (previous) {
|
|
697
|
+
const oldScopes = new Set(previous.scope.split(" "));
|
|
698
|
+
requireValue([...scopes].every((entry) => oldScopes.has(entry)), "LMM refresh tried to widen granted scope. Sign in again.");
|
|
699
|
+
requireValue(refresh !== previous.refresh, "LMM refresh did not rotate the refresh token. Sign in again.");
|
|
700
|
+
} else requireValue(APPLICATION_SCOPES.every((entry) => scopes.has(entry)), "LMM did not grant the required application scopes.");
|
|
701
|
+
return {
|
|
702
|
+
type: "oauth",
|
|
703
|
+
access,
|
|
704
|
+
refresh,
|
|
705
|
+
expires: startedAt + lifetime * 1e3,
|
|
706
|
+
lmm_issuer: this.http.issuer,
|
|
707
|
+
lmm_resource: this.http.resource,
|
|
708
|
+
lmm_session: previous?.lmm_session ?? randomUUID(),
|
|
709
|
+
scope
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/** Native production callback: no network and no storage outside Pi. */
|
|
713
|
+
async refreshBlocked() {
|
|
714
|
+
throw new LmmError("refresh_storage_unverified", REFRESH_BLOCKED);
|
|
715
|
+
}
|
|
716
|
+
async refresh(value, signal) {
|
|
717
|
+
const current = credential(value, this.http.issuer);
|
|
718
|
+
signal.throwIfAborted();
|
|
719
|
+
if (!this.refreshJournal) throw new LmmError("refresh_storage_unverified", REFRESH_BLOCKED);
|
|
720
|
+
await this.refreshJournal.begin(this.http.issuer, current.refresh);
|
|
721
|
+
signal.throwIfAborted();
|
|
722
|
+
return this.exchangeRefresh(current, signal);
|
|
723
|
+
}
|
|
724
|
+
/** Wire-level exchange only, for tests / a future reviewed host transaction adapter. Not wired into the extension. */
|
|
725
|
+
async exchangeRefresh(value, signal) {
|
|
726
|
+
const current = credential(value, this.http.issuer);
|
|
727
|
+
const startedAt = Date.now();
|
|
728
|
+
const response = await this.http.form("/api/oauth2/token", {
|
|
729
|
+
grant_type: "refresh_token",
|
|
730
|
+
client_id: this.clientId,
|
|
731
|
+
refresh_token: current.refresh,
|
|
732
|
+
resource: this.http.resource
|
|
733
|
+
}, signal);
|
|
734
|
+
return this.parseToken(response, startedAt, current);
|
|
735
|
+
}
|
|
736
|
+
/** Access-token revocation invalidates the whole family in the fixed LMM profile. No refresh is attempted. */
|
|
737
|
+
async revoke(access, signal) {
|
|
738
|
+
await this.http.form("/api/oauth2/revoke", {
|
|
739
|
+
client_id: this.clientId,
|
|
740
|
+
token: accessToken(access),
|
|
741
|
+
token_type_hint: "access_token"
|
|
742
|
+
}, signal);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region vendor/pi-lmm-provider/src/stream.ts
|
|
747
|
+
const streams = {
|
|
748
|
+
"openai-completions": openAICompletionsApi(),
|
|
749
|
+
"openai-responses": openAIResponsesApi(),
|
|
750
|
+
"anthropic-messages": anthropicMessagesApi()
|
|
751
|
+
};
|
|
752
|
+
const RETRY_BASE_DELAY_MS = 250;
|
|
753
|
+
const RETRY_MAX_DELAY_MS = 8e3;
|
|
754
|
+
function errorText(error) {
|
|
755
|
+
if (error instanceof Error) return error.message;
|
|
756
|
+
if (typeof error === "string") return error;
|
|
757
|
+
if (error && typeof error === "object" && "errorMessage" in error && typeof error.errorMessage === "string") return error.errorMessage;
|
|
758
|
+
return "";
|
|
759
|
+
}
|
|
760
|
+
function errorStatus(error, message = errorText(error)) {
|
|
761
|
+
if (error && typeof error === "object") for (const key of ["status", "statusCode"]) {
|
|
762
|
+
const value = error[key];
|
|
763
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
764
|
+
}
|
|
765
|
+
const match = message.match(/\b([45]\d{2})\b/);
|
|
766
|
+
return match ? Number(match[1]) : void 0;
|
|
767
|
+
}
|
|
768
|
+
function isTransportFailure(error) {
|
|
769
|
+
const message = errorText(error);
|
|
770
|
+
const status = errorStatus(error, message);
|
|
771
|
+
if (status !== void 0) return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
772
|
+
return /network|transport|disconnect|decod(?:e|ing)|response body|fetch failed|socket|timed out|stream ended before/i.test(message);
|
|
773
|
+
}
|
|
774
|
+
function publicErrorMessage(error, _retries = 0, outputStarted = false) {
|
|
775
|
+
const message = errorText(error);
|
|
776
|
+
if (/\babort(?:ed|ing)\b|cancel(?:led|ed)|request aborted/i.test(message)) return "LMM request cancelled.";
|
|
777
|
+
if (/IP_ACCESS_ROUTE_REJECTED|IP access policy/i.test(message)) return "LMM request was blocked by the current IP access policy. Change network or ask the administrator to allow this IP.";
|
|
778
|
+
const status = errorStatus(error, message);
|
|
779
|
+
if (status === 401) return "LMM authorization expired or was revoked. Run /login again.";
|
|
780
|
+
if (status === 403) return "This LMM account is not allowed to use the selected model. Run /login again or choose another model.";
|
|
781
|
+
if (status === 429) return "LMM rate limit reached. Wait a moment and retry.";
|
|
782
|
+
if (status !== void 0 && status >= 500) return `LMM upstream is temporarily unavailable (HTTP ${status}). Try again shortly.`;
|
|
783
|
+
if (isTransportFailure(error)) {
|
|
784
|
+
if (outputStarted) return "LMM stream disconnected after output started. The partial request was not replayed to avoid duplicate billing; try again.";
|
|
785
|
+
return "LMM stream disconnected before completion. The request was not replayed to avoid duplicate billing; try again manually.";
|
|
786
|
+
}
|
|
787
|
+
return "LMM model request failed. Check authorization, account access, and server availability.";
|
|
788
|
+
}
|
|
789
|
+
async function waitForRetry(delay, signal) {
|
|
790
|
+
if (delay <= 0) return;
|
|
791
|
+
await new Promise((resolve, reject) => {
|
|
792
|
+
const timer = setTimeout(() => {
|
|
793
|
+
signal?.removeEventListener("abort", abort);
|
|
794
|
+
resolve();
|
|
795
|
+
}, delay);
|
|
796
|
+
const abort = () => {
|
|
797
|
+
clearTimeout(timer);
|
|
798
|
+
signal?.removeEventListener("abort", abort);
|
|
799
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
800
|
+
};
|
|
801
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function bearerFromHeaders(headers) {
|
|
805
|
+
const values = Object.entries(headers ?? {}).filter(([key]) => key.toLowerCase() === "authorization").map(([, value]) => value);
|
|
806
|
+
requireValue(values.length === 1 && typeof values[0] === "string" && values[0].startsWith("Bearer "), "LMM model requests require native OAuth authorization.");
|
|
807
|
+
return accessToken(values[0].slice(7));
|
|
808
|
+
}
|
|
809
|
+
function errorMessage(model, aborted, error, retries = 0, outputStarted = false) {
|
|
810
|
+
return {
|
|
811
|
+
role: "assistant",
|
|
812
|
+
content: [],
|
|
813
|
+
api: model.api,
|
|
814
|
+
provider: "lmm",
|
|
815
|
+
model: model.id,
|
|
816
|
+
usage: {
|
|
817
|
+
input: 0,
|
|
818
|
+
output: 0,
|
|
819
|
+
cacheRead: 0,
|
|
820
|
+
cacheWrite: 0,
|
|
821
|
+
totalTokens: 0,
|
|
822
|
+
cost: {
|
|
823
|
+
input: 0,
|
|
824
|
+
output: 0,
|
|
825
|
+
cacheRead: 0,
|
|
826
|
+
cacheWrite: 0,
|
|
827
|
+
total: 0
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
stopReason: aborted ? "aborted" : "error",
|
|
831
|
+
errorMessage: aborted ? "LMM request cancelled." : publicErrorMessage(error, retries, outputStarted),
|
|
832
|
+
timestamp: Date.now()
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function rebind(message, model) {
|
|
836
|
+
const result = {
|
|
837
|
+
...message,
|
|
838
|
+
provider: "lmm",
|
|
839
|
+
model: model.id,
|
|
840
|
+
api: model.api
|
|
841
|
+
};
|
|
842
|
+
if (result.stopReason === "error" || result.stopReason === "aborted") result.errorMessage = result.stopReason === "aborted" ? "LMM request cancelled." : publicErrorMessage(result.errorMessage);
|
|
843
|
+
return result;
|
|
844
|
+
}
|
|
845
|
+
function rebindEvent(event, model) {
|
|
846
|
+
if (event.type === "done") return {
|
|
847
|
+
...event,
|
|
848
|
+
message: rebind(event.message, model)
|
|
849
|
+
};
|
|
850
|
+
if (event.type === "error") return {
|
|
851
|
+
...event,
|
|
852
|
+
error: rebind(event.error, model)
|
|
853
|
+
};
|
|
854
|
+
if ("partial" in event) return {
|
|
855
|
+
...event,
|
|
856
|
+
partial: rebind(event.partial, model)
|
|
857
|
+
};
|
|
858
|
+
return event;
|
|
859
|
+
}
|
|
860
|
+
function createRelay(http, hooks) {
|
|
861
|
+
const run = (simple, selected, context, options = {}) => {
|
|
862
|
+
const output = createAssistantMessageEventStream();
|
|
863
|
+
(async () => {
|
|
864
|
+
let access;
|
|
865
|
+
let sent = false;
|
|
866
|
+
let attempt = 0;
|
|
867
|
+
let streamStarted = false;
|
|
868
|
+
try {
|
|
869
|
+
requireValue(options.apiKey === void 0, "An API key must not be combined with LMM OAuth.");
|
|
870
|
+
access = bearerFromHeaders(options.headers);
|
|
871
|
+
const admission = hooks.lookup(selected.id, access);
|
|
872
|
+
requireValue(admission?.model, "This LMM model is no longer admitted by the current account catalog.");
|
|
873
|
+
const { entry, model } = admission;
|
|
874
|
+
requireValue(model && selected.provider === "lmm" && selected.api === model.api);
|
|
875
|
+
if (options.maxTokens !== void 0) requireValue(Number.isSafeInteger(options.maxTokens) && options.maxTokens > 0 && options.maxTokens <= model.maxTokens, "Requested output exceeds the verified LMM model capability.");
|
|
876
|
+
const api = model.api;
|
|
877
|
+
const wire = {
|
|
878
|
+
...structuredClone(model),
|
|
879
|
+
id: entry.upstream_model,
|
|
880
|
+
headers: void 0
|
|
881
|
+
};
|
|
882
|
+
const path = api === "anthropic-messages" ? "/v1/messages" : api === "openai-responses" ? "/v1/responses" : "/v1/chat/completions";
|
|
883
|
+
const headers = {
|
|
884
|
+
authorization: `Bearer ${access}`,
|
|
885
|
+
"X-LMM-Group": entry.group_id,
|
|
886
|
+
"x-api-key": null
|
|
887
|
+
};
|
|
888
|
+
const authorizedAccess = access;
|
|
889
|
+
const guardedFetch = async (input, init) => {
|
|
890
|
+
const target = input instanceof Request ? input.url : String(input);
|
|
891
|
+
const endpoint = `${http.issuer}${path}`;
|
|
892
|
+
const sdkBeta = api === "anthropic-messages" && target === `${endpoint}?beta=true`;
|
|
893
|
+
requireValue(target === endpoint || sdkBeta, "LMM relay destination does not match its advertised protocol.");
|
|
894
|
+
const actual = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
|
|
895
|
+
for (const name of [
|
|
896
|
+
"x-api-key",
|
|
897
|
+
"api-key",
|
|
898
|
+
"proxy-authorization",
|
|
899
|
+
"cookie"
|
|
900
|
+
]) actual.delete(name);
|
|
901
|
+
actual.set("authorization", `Bearer ${authorizedAccess}`);
|
|
902
|
+
actual.set("x-lmm-group", entry.group_id);
|
|
903
|
+
sent = true;
|
|
904
|
+
const destination = sdkBeta ? input instanceof Request ? new Request(endpoint, input) : endpoint : input;
|
|
905
|
+
return http.fetch(destination, {
|
|
906
|
+
...init,
|
|
907
|
+
headers: actual,
|
|
908
|
+
redirect: "error",
|
|
909
|
+
credentials: "omit"
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
const originalOnPayload = options.onPayload;
|
|
913
|
+
const wireOptions = {
|
|
914
|
+
...options,
|
|
915
|
+
apiKey: void 0,
|
|
916
|
+
headers,
|
|
917
|
+
env: {},
|
|
918
|
+
fetch: guardedFetch,
|
|
919
|
+
maxTokens: options.maxTokens ?? model.maxTokens,
|
|
920
|
+
maxRetries: 0,
|
|
921
|
+
onPayload: async (payload, payloadModel) => {
|
|
922
|
+
return {
|
|
923
|
+
...object(await originalOnPayload?.(payload, payloadModel) ?? payload),
|
|
924
|
+
model: entry.upstream_model,
|
|
925
|
+
stream: true
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const wireContext = {
|
|
930
|
+
...context,
|
|
931
|
+
messages: context.messages.map((message) => message.role === "assistant" && message.provider === "lmm" && message.model === selected.id ? {
|
|
932
|
+
...message,
|
|
933
|
+
model: entry.upstream_model
|
|
934
|
+
} : message)
|
|
935
|
+
};
|
|
936
|
+
while (true) {
|
|
937
|
+
const source = simple ? streams[api].streamSimple(wire, wireContext, wireOptions) : streams[api].stream(wire, wireContext, wireOptions);
|
|
938
|
+
let retry = false;
|
|
939
|
+
try {
|
|
940
|
+
for await (const event of source) {
|
|
941
|
+
if (event.type === "start" || event.type !== "error" && event.type !== "done") streamStarted = true;
|
|
942
|
+
if (event.type === "error") {
|
|
943
|
+
const shouldRetry = !streamStarted && isTransportFailure(event.error) && attempt < 0;
|
|
944
|
+
const error = rebind(event.error, selected);
|
|
945
|
+
if (shouldRetry) {
|
|
946
|
+
attempt += 1;
|
|
947
|
+
await waitForRetry(Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** (attempt - 1)), options.signal);
|
|
948
|
+
retry = true;
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
error.errorMessage = publicErrorMessage(event.error, attempt, streamStarted);
|
|
952
|
+
output.push({
|
|
953
|
+
...event,
|
|
954
|
+
error
|
|
955
|
+
});
|
|
956
|
+
output.end(error);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
output.push(rebindEvent(event, selected));
|
|
960
|
+
}
|
|
961
|
+
} catch (error) {
|
|
962
|
+
if (!streamStarted && isTransportFailure(error) && attempt < 0) {
|
|
963
|
+
attempt += 1;
|
|
964
|
+
await waitForRetry(Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** (attempt - 1)), options.signal);
|
|
965
|
+
retry = true;
|
|
966
|
+
} else throw error;
|
|
967
|
+
}
|
|
968
|
+
if (retry) continue;
|
|
969
|
+
output.end(rebind(await source.result(), selected));
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
const message = errorMessage(selected, options.signal?.aborted === true, error, attempt, streamStarted);
|
|
974
|
+
if (error instanceof LmmError) message.errorMessage = error.message;
|
|
975
|
+
output.push({
|
|
976
|
+
type: "error",
|
|
977
|
+
reason: options.signal?.aborted ? "aborted" : "error",
|
|
978
|
+
error: message
|
|
979
|
+
});
|
|
980
|
+
output.end(message);
|
|
981
|
+
} finally {
|
|
982
|
+
if (sent && access) await hooks.onFinish(access).catch(() => {});
|
|
983
|
+
}
|
|
984
|
+
})();
|
|
985
|
+
return output;
|
|
986
|
+
};
|
|
987
|
+
return {
|
|
988
|
+
stream: (model, context, options) => run(false, model, context, options),
|
|
989
|
+
streamSimple: (model, context, options) => run(true, model, context, options)
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region vendor/pi-lmm-provider/src/provider.ts
|
|
994
|
+
function fingerprint(access) {
|
|
995
|
+
return createHash("sha256").update(access).digest("hex");
|
|
996
|
+
}
|
|
997
|
+
const CACHE_MAX_AGE_MS = 864e5;
|
|
998
|
+
const CACHE_FUTURE_SKEW_MS = 3e5;
|
|
999
|
+
function cacheTag(value) {
|
|
1000
|
+
return `lmm-session-sha256:${createHash("sha256").update(`${value.lmm_issuer}\n${value.lmm_session}`).digest("hex")}`;
|
|
1001
|
+
}
|
|
1002
|
+
function decodeNativePart(value) {
|
|
1003
|
+
return value.replaceAll("%20%2F%20", " / ").replaceAll("%25", "%");
|
|
1004
|
+
}
|
|
1005
|
+
function validCost(cost) {
|
|
1006
|
+
const validRates = (rates) => [
|
|
1007
|
+
rates.input,
|
|
1008
|
+
rates.output,
|
|
1009
|
+
rates.cacheRead,
|
|
1010
|
+
rates.cacheWrite
|
|
1011
|
+
].every((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
|
|
1012
|
+
return validRates(cost) && (cost.tiers ?? []).every((tier) => Number.isSafeInteger(tier.inputTokensAbove) && tier.inputTokensAbove >= 0 && validRates(tier));
|
|
1013
|
+
}
|
|
1014
|
+
function cachedEntry(model, updatedAt) {
|
|
1015
|
+
try {
|
|
1016
|
+
if (model.provider !== "lmm" || !SUPPORTED_APIS.includes(model.api)) return void 0;
|
|
1017
|
+
const parts = model.id.split(" / ");
|
|
1018
|
+
if (parts.length !== 2) return void 0;
|
|
1019
|
+
const group = text(decodeNativePart(parts[0]));
|
|
1020
|
+
const upstream = text(decodeNativePart(parts[1]), 512);
|
|
1021
|
+
if (nativeModelId({
|
|
1022
|
+
group,
|
|
1023
|
+
upstream_model: upstream
|
|
1024
|
+
}) !== model.id) return void 0;
|
|
1025
|
+
if (!validCost(model.cost)) return void 0;
|
|
1026
|
+
const pricing = {
|
|
1027
|
+
currency: "USD",
|
|
1028
|
+
unit: "million_tokens",
|
|
1029
|
+
price_basis: "configured_base_rates",
|
|
1030
|
+
group_multiplier: null,
|
|
1031
|
+
trust_multiplier: null,
|
|
1032
|
+
input: model.cost.input,
|
|
1033
|
+
output: model.cost.output,
|
|
1034
|
+
cache_read: model.cost.cacheRead,
|
|
1035
|
+
cache_write: model.cost.cacheWrite,
|
|
1036
|
+
request: null,
|
|
1037
|
+
final_cost_depends_on_usage: true,
|
|
1038
|
+
updated_at: updatedAt
|
|
1039
|
+
};
|
|
1040
|
+
const group_id = base64url(group);
|
|
1041
|
+
return {
|
|
1042
|
+
id: `lmm:${group_id}:${base64url(upstream)}`,
|
|
1043
|
+
group_id,
|
|
1044
|
+
group,
|
|
1045
|
+
upstream_model: upstream,
|
|
1046
|
+
name: `${group} / ${upstream}`,
|
|
1047
|
+
apis: [model.api],
|
|
1048
|
+
pricing,
|
|
1049
|
+
native_cost: structuredClone(model.cost)
|
|
1050
|
+
};
|
|
1051
|
+
} catch {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
var LmmIntegration = class {
|
|
1056
|
+
http;
|
|
1057
|
+
oauth;
|
|
1058
|
+
provider;
|
|
1059
|
+
capabilities;
|
|
1060
|
+
notify;
|
|
1061
|
+
shutdown = new AbortController();
|
|
1062
|
+
current;
|
|
1063
|
+
pendingLogin;
|
|
1064
|
+
revoked = /* @__PURE__ */ new Set();
|
|
1065
|
+
balancePending;
|
|
1066
|
+
authSnapshotPending;
|
|
1067
|
+
epoch = 0;
|
|
1068
|
+
constructor(options = {}) {
|
|
1069
|
+
this.http = new LmmHttp(options);
|
|
1070
|
+
this.oauth = new LmmOAuth(this.http, options.loginTimeoutMs, options.refreshJournalDirectory, options.clientId, options.hostName);
|
|
1071
|
+
this.capabilities = options.capabilities ?? resolveKnownCapabilities;
|
|
1072
|
+
this.notify = options.onStatus ?? (() => {});
|
|
1073
|
+
const relay = createRelay(this.http, {
|
|
1074
|
+
lookup: (id, access) => this.lookup(id, access),
|
|
1075
|
+
onFinish: (access) => this.refreshBalance(access)
|
|
1076
|
+
});
|
|
1077
|
+
this.provider = {
|
|
1078
|
+
id: "lmm",
|
|
1079
|
+
name: "LMM",
|
|
1080
|
+
baseUrl: this.http.issuer,
|
|
1081
|
+
auth: { oauth: {
|
|
1082
|
+
name: "LMM browser login",
|
|
1083
|
+
login: async (interaction) => {
|
|
1084
|
+
this.clear();
|
|
1085
|
+
const epoch = this.epoch;
|
|
1086
|
+
const issued = await this.oauth.login({
|
|
1087
|
+
...interaction,
|
|
1088
|
+
signal: AbortSignal.any([interaction.signal, this.shutdown.signal])
|
|
1089
|
+
});
|
|
1090
|
+
try {
|
|
1091
|
+
const snapshot = await this.fetchSnapshot(issued, boundedSignal(interaction.signal));
|
|
1092
|
+
if (epoch === this.epoch) this.pendingLogin = snapshot;
|
|
1093
|
+
} catch {
|
|
1094
|
+
interaction.notify({
|
|
1095
|
+
type: "progress",
|
|
1096
|
+
message: "LMM login succeeded, but its catalog or balance is unavailable. Use /lmm-prices to retry discovery; unknown models are not registered."
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
interaction.notify({
|
|
1100
|
+
type: "progress",
|
|
1101
|
+
message: `LMM uses ${options.hostName ?? "Pi"}'s native model selector. Entries without verified capabilities or truthful native pricing remain read-only. Automatic refresh is fenced by a durable local journal; signing out is local only.`
|
|
1102
|
+
});
|
|
1103
|
+
return issued;
|
|
1104
|
+
},
|
|
1105
|
+
refresh: async (value, signal) => this.oauth.refresh(value, AbortSignal.any([signal, this.shutdown.signal])),
|
|
1106
|
+
toAuth: async (value) => {
|
|
1107
|
+
const current = credential(value, this.http.issuer);
|
|
1108
|
+
if (this.revoked.has(current.lmm_session)) throw new LmmError("revoked", "This LMM grant was revoked. Use /logout and then /login.");
|
|
1109
|
+
await this.refreshSnapshotForAuth(current);
|
|
1110
|
+
return {
|
|
1111
|
+
headers: { authorization: `Bearer ${current.access}` },
|
|
1112
|
+
baseUrl: this.http.issuer
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
} },
|
|
1116
|
+
getModels: () => structuredClone(this.current?.admissions.flatMap(({ model }) => model ? [model] : []) ?? []),
|
|
1117
|
+
filterModels: (models, stored) => {
|
|
1118
|
+
const current = this.maybeCredential(stored);
|
|
1119
|
+
return current && this.current?.session === current.lmm_session && this.current.accessHash === fingerprint(current.access) && this.current.expires > Date.now() ? models.filter((model) => this.current?.admissions.some((item) => item.model?.id === model.id)) : [];
|
|
1120
|
+
},
|
|
1121
|
+
refreshModels: (context) => this.refreshModels(context),
|
|
1122
|
+
stream: relay.stream,
|
|
1123
|
+
streamSimple: relay.streamSimple
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
maybeCredential(value) {
|
|
1127
|
+
try {
|
|
1128
|
+
const result = credential(value, this.http.issuer);
|
|
1129
|
+
return this.revoked.has(result.lmm_session) ? void 0 : result;
|
|
1130
|
+
} catch {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
clear() {
|
|
1135
|
+
this.epoch++;
|
|
1136
|
+
this.current = void 0;
|
|
1137
|
+
this.pendingLogin = void 0;
|
|
1138
|
+
this.balancePending = void 0;
|
|
1139
|
+
this.authSnapshotPending = void 0;
|
|
1140
|
+
this.notify(void 0);
|
|
1141
|
+
}
|
|
1142
|
+
status() {
|
|
1143
|
+
if (!this.current) return this.notify(void 0);
|
|
1144
|
+
const admitted = this.current.admissions.filter((item) => item.model).length;
|
|
1145
|
+
this.notify(`${balanceStatus(this.current.balance, this.current.balanceStale)}${this.current.catalogStale ? " · cached models" : ""}${admitted === 0 ? " · models gated" : ""}`);
|
|
1146
|
+
}
|
|
1147
|
+
async fetchSnapshot(value, signal) {
|
|
1148
|
+
const scope = new Set(value.scope.split(" "));
|
|
1149
|
+
const snapshot = {
|
|
1150
|
+
session: value.lmm_session,
|
|
1151
|
+
accessHash: fingerprint(value.access),
|
|
1152
|
+
expires: value.expires,
|
|
1153
|
+
admissions: [],
|
|
1154
|
+
catalogUpdatedAt: 0,
|
|
1155
|
+
checkedAt: Date.now(),
|
|
1156
|
+
catalogStale: false,
|
|
1157
|
+
balanceStale: false
|
|
1158
|
+
};
|
|
1159
|
+
const parsed = parseCatalog(await this.http.bearer("/api/oauth2/catalog", value.access, signal), this.http.resource, value.scope);
|
|
1160
|
+
snapshot.catalogUpdatedAt = parsed.updated_at;
|
|
1161
|
+
if (scope.has("models:invoke")) snapshot.admissions = admitCatalog(parsed, this.http.issuer, this.capabilities);
|
|
1162
|
+
else snapshot.admissions = parsed.models.map((entry) => ({
|
|
1163
|
+
entry,
|
|
1164
|
+
reason: "models:invoke was not granted; read-only."
|
|
1165
|
+
}));
|
|
1166
|
+
if (scope.has("balance:read")) try {
|
|
1167
|
+
snapshot.balance = parseBalance(await this.http.bearer("/api/oauth2/balance", value.access, signal));
|
|
1168
|
+
} catch {
|
|
1169
|
+
snapshot.balanceStale = true;
|
|
1170
|
+
}
|
|
1171
|
+
return snapshot;
|
|
1172
|
+
}
|
|
1173
|
+
cache(snapshot, value) {
|
|
1174
|
+
return {
|
|
1175
|
+
models: snapshot.admissions.flatMap(({ model }) => model ? [structuredClone(model)] : []),
|
|
1176
|
+
checkedAt: snapshot.checkedAt,
|
|
1177
|
+
lastModified: snapshot.catalogUpdatedAt * 1e3,
|
|
1178
|
+
etag: cacheTag(value)
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
restore(stored, value) {
|
|
1182
|
+
const checkedAt = stored?.checkedAt;
|
|
1183
|
+
if (stored?.etag !== cacheTag(value) || typeof checkedAt !== "number" || !Number.isFinite(checkedAt) || checkedAt > Date.now() + CACHE_FUTURE_SKEW_MS || Date.now() - checkedAt > CACHE_MAX_AGE_MS) return void 0;
|
|
1184
|
+
const updatedAt = typeof stored.lastModified === "number" && Number.isSafeInteger(stored.lastModified) && stored.lastModified >= 0 ? Math.floor(stored.lastModified / 1e3) : Math.floor(checkedAt / 1e3);
|
|
1185
|
+
const entries = stored.models.map((model) => cachedEntry(model, updatedAt));
|
|
1186
|
+
if (entries.some((entry) => !entry)) return void 0;
|
|
1187
|
+
const complete = entries;
|
|
1188
|
+
const scope = new Set(value.scope.split(" "));
|
|
1189
|
+
const admitted = complete.filter((entry) => scope.has(`group:${entry.group_id}`));
|
|
1190
|
+
const admissions = scope.has("models:invoke") ? admitCatalog({
|
|
1191
|
+
schema_version: 1,
|
|
1192
|
+
resource: this.http.resource,
|
|
1193
|
+
updated_at: updatedAt,
|
|
1194
|
+
groups: [],
|
|
1195
|
+
models: admitted
|
|
1196
|
+
}, this.http.issuer, this.capabilities) : [];
|
|
1197
|
+
for (const admission of admissions) {
|
|
1198
|
+
const cached = stored.models.find((model) => model.id === admission.model?.id);
|
|
1199
|
+
if (admission.model && cached) admission.model.name = text(cached.name);
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
session: value.lmm_session,
|
|
1203
|
+
accessHash: fingerprint(value.access),
|
|
1204
|
+
expires: value.expires,
|
|
1205
|
+
admissions,
|
|
1206
|
+
catalogUpdatedAt: updatedAt,
|
|
1207
|
+
checkedAt,
|
|
1208
|
+
catalogStale: true,
|
|
1209
|
+
balanceStale: true
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
rebind(snapshot, value) {
|
|
1213
|
+
const scope = new Set(value.scope.split(" "));
|
|
1214
|
+
return {
|
|
1215
|
+
...snapshot,
|
|
1216
|
+
session: value.lmm_session,
|
|
1217
|
+
accessHash: fingerprint(value.access),
|
|
1218
|
+
expires: value.expires,
|
|
1219
|
+
admissions: scope.has("models:invoke") ? snapshot.admissions.filter(({ entry }) => scope.has(`group:${entry.group_id}`)) : [],
|
|
1220
|
+
catalogStale: true,
|
|
1221
|
+
balance: void 0,
|
|
1222
|
+
balanceStale: true
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
async refreshSnapshotForAuth(value) {
|
|
1226
|
+
const hash = fingerprint(value.access);
|
|
1227
|
+
if (this.current?.session === value.lmm_session && this.current.accessHash === hash && this.current.expires > Date.now()) return;
|
|
1228
|
+
if (this.authSnapshotPending?.hash === hash) return this.authSnapshotPending.task;
|
|
1229
|
+
const fallback = this.current?.session === value.lmm_session ? this.rebind(this.current, value) : void 0;
|
|
1230
|
+
const epoch = ++this.epoch;
|
|
1231
|
+
const task = (async () => {
|
|
1232
|
+
try {
|
|
1233
|
+
const snapshot = await this.fetchSnapshot(value, boundedSignal(this.shutdown.signal));
|
|
1234
|
+
if (epoch === this.epoch && !this.shutdown.signal.aborted) {
|
|
1235
|
+
this.current = snapshot;
|
|
1236
|
+
this.pendingLogin = void 0;
|
|
1237
|
+
this.status();
|
|
1238
|
+
}
|
|
1239
|
+
} catch {
|
|
1240
|
+
if (fallback && epoch === this.epoch && !this.shutdown.signal.aborted) {
|
|
1241
|
+
this.current = fallback;
|
|
1242
|
+
this.status();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
})();
|
|
1246
|
+
this.authSnapshotPending = {
|
|
1247
|
+
hash,
|
|
1248
|
+
task
|
|
1249
|
+
};
|
|
1250
|
+
try {
|
|
1251
|
+
await task;
|
|
1252
|
+
} finally {
|
|
1253
|
+
if (this.authSnapshotPending?.task === task) this.authSnapshotPending = void 0;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async refreshModels(context) {
|
|
1257
|
+
const value = this.maybeCredential(context.credential);
|
|
1258
|
+
if (!value) {
|
|
1259
|
+
await context.publish({
|
|
1260
|
+
persist: null,
|
|
1261
|
+
update: () => this.clear()
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (!context.allowNetwork) {
|
|
1266
|
+
const hash = fingerprint(value.access);
|
|
1267
|
+
const snapshot = (this.pendingLogin?.session === value.lmm_session && this.pendingLogin.accessHash === hash ? this.pendingLogin : this.current?.session === value.lmm_session ? this.rebind(this.current, value) : void 0) ?? this.restore(context.stored, value);
|
|
1268
|
+
await context.publish({
|
|
1269
|
+
persist: snapshot ? this.cache(snapshot, value) : null,
|
|
1270
|
+
update: () => {
|
|
1271
|
+
if (this.current?.session !== value.lmm_session) this.epoch++;
|
|
1272
|
+
this.current = snapshot;
|
|
1273
|
+
this.pendingLogin = void 0;
|
|
1274
|
+
this.status();
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const fallback = this.current?.session === value.lmm_session ? this.rebind(this.current, value) : this.restore(context.stored, value);
|
|
1280
|
+
if (fallback) await context.publish({ update: () => {
|
|
1281
|
+
this.current = fallback;
|
|
1282
|
+
this.pendingLogin = void 0;
|
|
1283
|
+
this.status();
|
|
1284
|
+
} });
|
|
1285
|
+
const signal = AbortSignal.any([context.signal, this.shutdown.signal]);
|
|
1286
|
+
this.epoch++;
|
|
1287
|
+
const epoch = this.epoch;
|
|
1288
|
+
const snapshot = await this.fetchSnapshot(value, signal);
|
|
1289
|
+
await context.publish({
|
|
1290
|
+
persist: this.cache(snapshot, value),
|
|
1291
|
+
update: () => {
|
|
1292
|
+
if (epoch !== this.epoch || this.shutdown.signal.aborted) return;
|
|
1293
|
+
this.current = snapshot;
|
|
1294
|
+
this.pendingLogin = void 0;
|
|
1295
|
+
this.status();
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
lookup(modelId, access) {
|
|
1300
|
+
if (!this.current || this.current.expires <= Date.now() || this.current.accessHash !== fingerprint(access)) return void 0;
|
|
1301
|
+
return this.current.admissions.find((item) => item.model?.id === modelId || item.entry.id === modelId);
|
|
1302
|
+
}
|
|
1303
|
+
modelForLegacyId(id) {
|
|
1304
|
+
const model = this.current?.admissions.find((item) => item.entry.id === id)?.model;
|
|
1305
|
+
return model ? structuredClone(model) : void 0;
|
|
1306
|
+
}
|
|
1307
|
+
async refreshBalance(access, signal) {
|
|
1308
|
+
const hash = fingerprint(access);
|
|
1309
|
+
const snapshot = this.current;
|
|
1310
|
+
if (!snapshot || snapshot.accessHash !== hash) return;
|
|
1311
|
+
if (this.balancePending?.hash === hash) return this.balancePending.task;
|
|
1312
|
+
const epoch = this.epoch;
|
|
1313
|
+
const task = (async () => {
|
|
1314
|
+
try {
|
|
1315
|
+
const balance = parseBalance(await this.http.bearer("/api/oauth2/balance", access, boundedSignal(AbortSignal.any([...signal ? [signal] : [], this.shutdown.signal]))));
|
|
1316
|
+
if (epoch === this.epoch && this.current === snapshot) {
|
|
1317
|
+
snapshot.balance = balance;
|
|
1318
|
+
snapshot.balanceStale = false;
|
|
1319
|
+
}
|
|
1320
|
+
} catch {
|
|
1321
|
+
if (epoch === this.epoch && this.current === snapshot) snapshot.balanceStale = true;
|
|
1322
|
+
}
|
|
1323
|
+
if (epoch === this.epoch && this.current === snapshot) this.status();
|
|
1324
|
+
})();
|
|
1325
|
+
this.balancePending = {
|
|
1326
|
+
hash,
|
|
1327
|
+
task
|
|
1328
|
+
};
|
|
1329
|
+
try {
|
|
1330
|
+
await task;
|
|
1331
|
+
} finally {
|
|
1332
|
+
if (this.balancePending?.task === task) this.balancePending = void 0;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
prices(filter = "") {
|
|
1336
|
+
return this.current ? priceReport(this.current.admissions, filter) : "No account-scoped LMM catalog. Use /login, then retry /lmm-prices. No models or capabilities are guessed.";
|
|
1337
|
+
}
|
|
1338
|
+
async revoke(access, signal) {
|
|
1339
|
+
const snapshot = this.current;
|
|
1340
|
+
if (snapshot && snapshot.accessHash !== fingerprint(access)) throw new LmmError("account_changed", "LMM account changed; retry from the current account.");
|
|
1341
|
+
await this.oauth.revoke(access, signal);
|
|
1342
|
+
if (snapshot) this.revoked.add(snapshot.session);
|
|
1343
|
+
if (this.current === snapshot) this.clear();
|
|
1344
|
+
}
|
|
1345
|
+
dispose() {
|
|
1346
|
+
this.shutdown.abort();
|
|
1347
|
+
this.clear();
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region src/web-auth.ts
|
|
1352
|
+
const AUTH_CHANNEL = "/api";
|
|
1353
|
+
const ATTEMPT_MS = 3e5;
|
|
1354
|
+
const fail = (code, message) => ({
|
|
1355
|
+
ok: false,
|
|
1356
|
+
error: {
|
|
1357
|
+
code,
|
|
1358
|
+
message,
|
|
1359
|
+
details: {}
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
const ok = (value) => ({
|
|
1363
|
+
ok: true,
|
|
1364
|
+
value
|
|
1365
|
+
});
|
|
1366
|
+
/** Only fixed, credential-free messages may cross the browser RPC boundary. */
|
|
1367
|
+
function publicAuthError(error) {
|
|
1368
|
+
switch (error && typeof error === "object" && "code" in error ? error.code : void 0) {
|
|
1369
|
+
case "callback_unavailable": return "DSH could not listen for the browser callback on 127.0.0.1. Check the local firewall and try again.";
|
|
1370
|
+
case "oauth_denied": return "LMM access was denied. Start sign-in again to retry.";
|
|
1371
|
+
case "transport_error": return "DSH could not reach LMM. Check your connection and try again.";
|
|
1372
|
+
case "invalid_response": return "LMM returned an invalid authorization response. Update the plugin and try again.";
|
|
1373
|
+
case "NOT_COMMITTED": return "DSH did not save the LMM login. Restart DSH and try again.";
|
|
1374
|
+
case "ALREADY_IN_FLIGHT": return "Another LMM sign-in is already running. Finish or cancel that attempt first.";
|
|
1375
|
+
default: return "LMM sign-in failed. Try again; if it persists, check the DSH Host log.";
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/** The host's authenticated Connection transport owns Host/Origin/cookie checks. */
|
|
1379
|
+
function mountBrowserAuth(ctx, key) {
|
|
1380
|
+
ctx.inject([
|
|
1381
|
+
"connection",
|
|
1382
|
+
"authorization",
|
|
1383
|
+
"credentials",
|
|
1384
|
+
"webServer"
|
|
1385
|
+
], (web) => {
|
|
1386
|
+
let current;
|
|
1387
|
+
const owned = (payload) => {
|
|
1388
|
+
if (payload === null || typeof payload !== "object") return;
|
|
1389
|
+
return "attempt" in payload && payload.attempt === current?.id ? current : void 0;
|
|
1390
|
+
};
|
|
1391
|
+
const view = (attempt) => ({
|
|
1392
|
+
attempt: attempt.id,
|
|
1393
|
+
state: attempt.state,
|
|
1394
|
+
notices: attempt.notices,
|
|
1395
|
+
...attempt.prompt === void 0 ? {} : { prompt: attempt.prompt },
|
|
1396
|
+
...attempt.error === void 0 ? {} : { error: attempt.error }
|
|
1397
|
+
});
|
|
1398
|
+
const stop = () => {
|
|
1399
|
+
current?.controller.abort();
|
|
1400
|
+
};
|
|
1401
|
+
web.effect(() => stop);
|
|
1402
|
+
const dispatch = async (endpoint, payload) => {
|
|
1403
|
+
if (endpoint === "status") return ok({
|
|
1404
|
+
signedIn: (await web.credentials.readRecord(key))?.kind === "grant",
|
|
1405
|
+
busy: current?.state === "pending"
|
|
1406
|
+
});
|
|
1407
|
+
if (endpoint === "begin") {
|
|
1408
|
+
if (current?.state === "pending") return fail("BUSY", "LMM sign-in is already running.");
|
|
1409
|
+
const attempt = {
|
|
1410
|
+
id: randomUUID(),
|
|
1411
|
+
state: "pending",
|
|
1412
|
+
controller: new AbortController(),
|
|
1413
|
+
notices: []
|
|
1414
|
+
};
|
|
1415
|
+
current = attempt;
|
|
1416
|
+
const timer = setTimeout(() => attempt.controller.abort(), ATTEMPT_MS);
|
|
1417
|
+
timer.unref();
|
|
1418
|
+
web.authorization.begin({
|
|
1419
|
+
key,
|
|
1420
|
+
method: "oauth",
|
|
1421
|
+
signal: attempt.controller.signal,
|
|
1422
|
+
interaction: {
|
|
1423
|
+
notify(notice) {
|
|
1424
|
+
attempt.notices = [...attempt.notices.slice(-7), notice];
|
|
1425
|
+
},
|
|
1426
|
+
prompt(prompt) {
|
|
1427
|
+
const signal = prompt.signal === void 0 ? attempt.controller.signal : AbortSignal.any([prompt.signal, attempt.controller.signal]);
|
|
1428
|
+
signal.throwIfAborted();
|
|
1429
|
+
return new Promise((resolve, reject) => {
|
|
1430
|
+
const id = randomUUID();
|
|
1431
|
+
const { signal: _signal, ...rest } = prompt;
|
|
1432
|
+
attempt.prompt = {
|
|
1433
|
+
...rest,
|
|
1434
|
+
id
|
|
1435
|
+
};
|
|
1436
|
+
const clear = () => {
|
|
1437
|
+
signal.removeEventListener("abort", abort);
|
|
1438
|
+
if (attempt.prompt?.id === id) {
|
|
1439
|
+
delete attempt.prompt;
|
|
1440
|
+
delete attempt.answer;
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
const abort = () => {
|
|
1444
|
+
clear();
|
|
1445
|
+
reject(/* @__PURE__ */ new Error("LMM sign-in prompt withdrawn."));
|
|
1446
|
+
};
|
|
1447
|
+
attempt.answer = (answer) => {
|
|
1448
|
+
clear();
|
|
1449
|
+
resolve(answer);
|
|
1450
|
+
};
|
|
1451
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}).then((outcome) => {
|
|
1456
|
+
attempt.state = outcome.status;
|
|
1457
|
+
}).catch((error) => {
|
|
1458
|
+
attempt.state = attempt.controller.signal.aborted ? "cancelled" : "failed";
|
|
1459
|
+
if (attempt.state === "failed") attempt.error = publicAuthError(error);
|
|
1460
|
+
}).finally(() => {
|
|
1461
|
+
clearTimeout(timer);
|
|
1462
|
+
delete attempt.prompt;
|
|
1463
|
+
delete attempt.answer;
|
|
1464
|
+
attempt.notices = [];
|
|
1465
|
+
});
|
|
1466
|
+
return ok(view(attempt));
|
|
1467
|
+
}
|
|
1468
|
+
if (endpoint === "logout") {
|
|
1469
|
+
if (current?.state === "pending") return fail("BUSY", "Cancel sign-in before signing out.");
|
|
1470
|
+
await web.credentials.deleteRecord(key);
|
|
1471
|
+
return ok({ signedIn: false });
|
|
1472
|
+
}
|
|
1473
|
+
if (![
|
|
1474
|
+
"poll",
|
|
1475
|
+
"answer",
|
|
1476
|
+
"cancel"
|
|
1477
|
+
].includes(endpoint)) return fail("NOT_FOUND", "Unknown LMM action.");
|
|
1478
|
+
const attempt = owned(payload);
|
|
1479
|
+
if (attempt === void 0) return fail("NOT_FOUND", "This sign-in attempt is unavailable. Start again.");
|
|
1480
|
+
if (endpoint === "answer") {
|
|
1481
|
+
const body = payload;
|
|
1482
|
+
const prompt = attempt.prompt;
|
|
1483
|
+
if (attempt.state !== "pending" || prompt === void 0 || body.prompt !== prompt.id || typeof body.value !== "string" || body.value.length > 4096) return fail("INVALID_PROMPT", "The sign-in question has changed.");
|
|
1484
|
+
if (prompt.kind === "select" && !prompt.options?.some((option) => option.id === body.value)) return fail("INVALID_ANSWER", "Choose an offered option.");
|
|
1485
|
+
attempt.answer?.(body.value);
|
|
1486
|
+
} else if (endpoint === "cancel") attempt.controller.abort();
|
|
1487
|
+
return ok(view(attempt));
|
|
1488
|
+
};
|
|
1489
|
+
for (const action of [
|
|
1490
|
+
"status",
|
|
1491
|
+
"begin",
|
|
1492
|
+
"poll",
|
|
1493
|
+
"answer",
|
|
1494
|
+
"cancel",
|
|
1495
|
+
"logout"
|
|
1496
|
+
]) web.effect(() => web.connection.fetch.register({
|
|
1497
|
+
path: `${AUTH_CHANNEL}/lmm-auth/${action}`,
|
|
1498
|
+
methods: ["POST"],
|
|
1499
|
+
requestBody: "buffered",
|
|
1500
|
+
async fetch(request) {
|
|
1501
|
+
let envelope;
|
|
1502
|
+
try {
|
|
1503
|
+
envelope = await request.json();
|
|
1504
|
+
} catch {
|
|
1505
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
1506
|
+
}
|
|
1507
|
+
if (envelope === null || typeof envelope !== "object") return new Response("Invalid request", { status: 400 });
|
|
1508
|
+
const message = envelope;
|
|
1509
|
+
if (message.type !== "client-request" || typeof message.rpcId !== "string" || message.rpcId.length > 256 || message.method !== `lmm-auth/${action}`) return new Response("Invalid request", { status: 400 });
|
|
1510
|
+
let result;
|
|
1511
|
+
try {
|
|
1512
|
+
result = await dispatch(action, message.payload);
|
|
1513
|
+
} catch {
|
|
1514
|
+
result = fail("INTERNAL", "LMM sign-in could not complete. Please try again.");
|
|
1515
|
+
}
|
|
1516
|
+
return Response.json({
|
|
1517
|
+
type: "server-response",
|
|
1518
|
+
rpcId: message.rpcId,
|
|
1519
|
+
result
|
|
1520
|
+
}, { headers: { "Cache-Control": "no-store" } });
|
|
1521
|
+
}
|
|
1522
|
+
}));
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/index.ts
|
|
1527
|
+
const name = "dsh-lmm-provider";
|
|
1528
|
+
const inject = ["llm", "credentials"];
|
|
1529
|
+
const DSH_CLIENT_ID = "lmm-dsh";
|
|
1530
|
+
const RECORD_KEY = credentialKey(name, "lmm");
|
|
1531
|
+
const REFRESH_TTL_MS = 6e4;
|
|
1532
|
+
const STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
1533
|
+
const MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
1534
|
+
const REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
1535
|
+
const REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
1536
|
+
async function waitWithSignal(task, signal) {
|
|
1537
|
+
if (signal === void 0) return task;
|
|
1538
|
+
signal.throwIfAborted();
|
|
1539
|
+
let abort = () => {};
|
|
1540
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
1541
|
+
abort = () => reject(signal.reason);
|
|
1542
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1543
|
+
});
|
|
1544
|
+
try {
|
|
1545
|
+
await Promise.race([task, aborted]);
|
|
1546
|
+
} finally {
|
|
1547
|
+
signal.removeEventListener("abort", abort);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function jsonImage(value) {
|
|
1551
|
+
if (Array.isArray(value)) return value.map((entry) => entry === void 0 ? null : jsonImage(entry));
|
|
1552
|
+
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
|
|
1553
|
+
const result = {};
|
|
1554
|
+
for (const [key, member] of Object.entries(value)) if (member !== void 0) result[key] = jsonImage(member);
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
return value;
|
|
1558
|
+
}
|
|
1559
|
+
function fromRecord(record) {
|
|
1560
|
+
if (record === void 0) return void 0;
|
|
1561
|
+
if (record.kind === "api-key") return {
|
|
1562
|
+
type: "api_key",
|
|
1563
|
+
...record.key === void 0 ? {} : { key: record.key },
|
|
1564
|
+
...record.env === void 0 ? {} : { env: { ...record.env } }
|
|
1565
|
+
};
|
|
1566
|
+
return record.payload;
|
|
1567
|
+
}
|
|
1568
|
+
function toRecord(value) {
|
|
1569
|
+
if (value.type === "api_key") return {
|
|
1570
|
+
kind: "api-key",
|
|
1571
|
+
...value.key === void 0 ? {} : { key: value.key },
|
|
1572
|
+
...value.env === void 0 ? {} : { env: { ...value.env } }
|
|
1573
|
+
};
|
|
1574
|
+
return {
|
|
1575
|
+
kind: "grant",
|
|
1576
|
+
payload: jsonImage(value)
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
/** Bridge pi-ai's OAuth store into DSH's durable credential record. */
|
|
1580
|
+
function credentialStoreFrom(ctx, key = RECORD_KEY) {
|
|
1581
|
+
const assertProvider = (providerId) => {
|
|
1582
|
+
if (providerId !== "lmm") throw new Error(`LMM credential store does not own provider ${JSON.stringify(providerId)}.`);
|
|
1583
|
+
};
|
|
1584
|
+
const checkSignal = (options) => options?.signal?.throwIfAborted();
|
|
1585
|
+
return {
|
|
1586
|
+
async read(providerId, options) {
|
|
1587
|
+
assertProvider(providerId);
|
|
1588
|
+
checkSignal(options);
|
|
1589
|
+
const value = fromRecord(await ctx.credentials.readRecord(key));
|
|
1590
|
+
checkSignal(options);
|
|
1591
|
+
return value;
|
|
1592
|
+
},
|
|
1593
|
+
async list(options) {
|
|
1594
|
+
checkSignal(options);
|
|
1595
|
+
const record = await ctx.credentials.readRecord(key);
|
|
1596
|
+
checkSignal(options);
|
|
1597
|
+
return record === void 0 ? [] : [{
|
|
1598
|
+
providerId: "lmm",
|
|
1599
|
+
type: record.kind === "api-key" ? "api_key" : "oauth"
|
|
1600
|
+
}];
|
|
1601
|
+
},
|
|
1602
|
+
async modify(providerId, mutate, options) {
|
|
1603
|
+
assertProvider(providerId);
|
|
1604
|
+
checkSignal(options);
|
|
1605
|
+
const stored = await ctx.credentials.modifyRecord(key, async (current) => {
|
|
1606
|
+
checkSignal(options);
|
|
1607
|
+
const next = await mutate(fromRecord(current));
|
|
1608
|
+
checkSignal(options);
|
|
1609
|
+
return next === void 0 ? void 0 : toRecord(next);
|
|
1610
|
+
});
|
|
1611
|
+
checkSignal(options);
|
|
1612
|
+
return fromRecord(stored);
|
|
1613
|
+
},
|
|
1614
|
+
async delete(providerId, options) {
|
|
1615
|
+
assertProvider(providerId);
|
|
1616
|
+
checkSignal(options);
|
|
1617
|
+
await ctx.credentials.deleteRecord(key);
|
|
1618
|
+
checkSignal(options);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function relay(event, session) {
|
|
1623
|
+
switch (event.type) {
|
|
1624
|
+
case "info": {
|
|
1625
|
+
const link = event.links?.[0];
|
|
1626
|
+
session.notify({
|
|
1627
|
+
message: event.message,
|
|
1628
|
+
...link === void 0 ? {} : { url: link.url }
|
|
1629
|
+
});
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
case "auth_url":
|
|
1633
|
+
session.notify({
|
|
1634
|
+
message: event.instructions ?? "Open this page to continue signing in.",
|
|
1635
|
+
url: event.url
|
|
1636
|
+
});
|
|
1637
|
+
return;
|
|
1638
|
+
case "device_code":
|
|
1639
|
+
session.notify({
|
|
1640
|
+
message: "Enter this code on the verification page to finish signing in.",
|
|
1641
|
+
url: event.verificationUri,
|
|
1642
|
+
code: event.userCode
|
|
1643
|
+
});
|
|
1644
|
+
return;
|
|
1645
|
+
case "progress": session.notify({ message: event.message });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function restate(prompt) {
|
|
1649
|
+
const signal = prompt.signal === void 0 ? {} : { signal: prompt.signal };
|
|
1650
|
+
if (prompt.type === "select") return {
|
|
1651
|
+
...signal,
|
|
1652
|
+
kind: "select",
|
|
1653
|
+
message: prompt.message,
|
|
1654
|
+
options: prompt.options
|
|
1655
|
+
};
|
|
1656
|
+
return {
|
|
1657
|
+
...signal,
|
|
1658
|
+
kind: prompt.type === "secret" ? "secret" : "text",
|
|
1659
|
+
message: prompt.message,
|
|
1660
|
+
...prompt.placeholder === void 0 ? {} : { placeholder: prompt.placeholder }
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function lmmProfile(integration) {
|
|
1664
|
+
return {
|
|
1665
|
+
provider: "lmm",
|
|
1666
|
+
displayName: "LMM",
|
|
1667
|
+
streamIdleTimeoutMs: STREAM_IDLE_TIMEOUT_MS,
|
|
1668
|
+
maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
|
|
1669
|
+
requestImagePixelBudget: REQUEST_IMAGE_PIXEL_BUDGET,
|
|
1670
|
+
requestImageMaxBytes: REQUEST_IMAGE_MAX_BYTES,
|
|
1671
|
+
retryPolicy: resolveRetryPolicy(void 0, "dsh-lmm-provider retryPolicy"),
|
|
1672
|
+
piProvider: integration.provider,
|
|
1673
|
+
modelErrors: /* @__PURE__ */ new Map(),
|
|
1674
|
+
configuredMaxTokens: /* @__PURE__ */ new Map()
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
var CatalogCoordinator = class {
|
|
1678
|
+
pending;
|
|
1679
|
+
checkedAt = 0;
|
|
1680
|
+
models;
|
|
1681
|
+
constructor(models) {
|
|
1682
|
+
this.models = models;
|
|
1683
|
+
}
|
|
1684
|
+
invalidate() {
|
|
1685
|
+
this.checkedAt = 0;
|
|
1686
|
+
}
|
|
1687
|
+
async refresh(signal, force = false, allowNetwork = true) {
|
|
1688
|
+
signal?.throwIfAborted();
|
|
1689
|
+
if (!force && Date.now() - this.checkedAt < REFRESH_TTL_MS) return;
|
|
1690
|
+
if (this.pending === void 0) {
|
|
1691
|
+
const pending = this.models.refresh({
|
|
1692
|
+
providers: ["lmm"],
|
|
1693
|
+
allowNetwork,
|
|
1694
|
+
force: allowNetwork ? force : void 0,
|
|
1695
|
+
signal: AbortSignal.timeout(3e4)
|
|
1696
|
+
}).then((result) => {
|
|
1697
|
+
const error = result.errors.get("lmm");
|
|
1698
|
+
if (error !== void 0) throw error;
|
|
1699
|
+
if (!result.aborted) this.checkedAt = Date.now();
|
|
1700
|
+
}).finally(() => {
|
|
1701
|
+
if (this.pending === pending) this.pending = void 0;
|
|
1702
|
+
});
|
|
1703
|
+
this.pending = pending;
|
|
1704
|
+
}
|
|
1705
|
+
await waitWithSignal(this.pending, signal);
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
var RefreshingAdapter = class extends LlmAdapter {
|
|
1709
|
+
adapter;
|
|
1710
|
+
catalog;
|
|
1711
|
+
constructor(adapter, catalog) {
|
|
1712
|
+
super();
|
|
1713
|
+
this.adapter = adapter;
|
|
1714
|
+
this.catalog = catalog;
|
|
1715
|
+
}
|
|
1716
|
+
providerInfo(provider) {
|
|
1717
|
+
return this.adapter.providerInfo(provider);
|
|
1718
|
+
}
|
|
1719
|
+
providerRetryPolicy(provider) {
|
|
1720
|
+
return this.adapter.providerRetryPolicy(provider);
|
|
1721
|
+
}
|
|
1722
|
+
async listModels(provider) {
|
|
1723
|
+
await this.catalog.refresh();
|
|
1724
|
+
return this.adapter.listModels(provider);
|
|
1725
|
+
}
|
|
1726
|
+
async resolveModel(provider, model, signal) {
|
|
1727
|
+
await this.catalog.refresh(signal);
|
|
1728
|
+
return this.adapter.resolveModel(provider, model, signal);
|
|
1729
|
+
}
|
|
1730
|
+
async prepareCall(provider, model, signal) {
|
|
1731
|
+
await this.catalog.refresh(signal);
|
|
1732
|
+
return this.adapter.prepareCall(provider, model, signal);
|
|
1733
|
+
}
|
|
1734
|
+
stream(options) {
|
|
1735
|
+
return this.streamAfterRefresh(options);
|
|
1736
|
+
}
|
|
1737
|
+
async *streamAfterRefresh(options) {
|
|
1738
|
+
await this.catalog.refresh(options.signal);
|
|
1739
|
+
yield* this.adapter.stream(options);
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
const authContext = {
|
|
1743
|
+
env: () => Promise.resolve(void 0),
|
|
1744
|
+
fileExists: () => Promise.resolve(false)
|
|
1745
|
+
};
|
|
1746
|
+
/** Mount the fixed LMM route and its browser OAuth flow into DSH. */
|
|
1747
|
+
function apply(ctx) {
|
|
1748
|
+
const integration = new LmmIntegration({
|
|
1749
|
+
clientId: DSH_CLIENT_ID,
|
|
1750
|
+
hostName: "DSH",
|
|
1751
|
+
loginTimeoutMs: 3e5,
|
|
1752
|
+
refreshJournalDirectory: join(resolveDshHome(), "lmm-refresh-journal")
|
|
1753
|
+
});
|
|
1754
|
+
const credentials = credentialStoreFrom(ctx);
|
|
1755
|
+
const models = createModels({
|
|
1756
|
+
credentials,
|
|
1757
|
+
authContext
|
|
1758
|
+
});
|
|
1759
|
+
models.setProvider(integration.provider);
|
|
1760
|
+
const catalog = new CatalogCoordinator(models);
|
|
1761
|
+
const profiles = /* @__PURE__ */ new Map([["lmm", lmmProfile(integration)]]);
|
|
1762
|
+
const adapter = new PiAiAdapter({
|
|
1763
|
+
profiles: () => profiles,
|
|
1764
|
+
resolveApiKey: () => Promise.resolve(void 0),
|
|
1765
|
+
auth: {
|
|
1766
|
+
credentials,
|
|
1767
|
+
authContext
|
|
1768
|
+
},
|
|
1769
|
+
resolveAttachments: () => ctx.get("attachments")
|
|
1770
|
+
});
|
|
1771
|
+
const registration = ctx.llm.registerAdapter(["lmm"], new RefreshingAdapter(adapter, catalog));
|
|
1772
|
+
ctx.inject(["authorization"], (authorized) => {
|
|
1773
|
+
authorized.authorization.registerFlow({
|
|
1774
|
+
key: RECORD_KEY,
|
|
1775
|
+
label: "LMM",
|
|
1776
|
+
methods: [{
|
|
1777
|
+
id: "oauth",
|
|
1778
|
+
label: "Sign in with LMM"
|
|
1779
|
+
}],
|
|
1780
|
+
async run(session) {
|
|
1781
|
+
await models.login("lmm", "oauth", {
|
|
1782
|
+
signal: session.signal,
|
|
1783
|
+
notify: (event) => relay(event, session),
|
|
1784
|
+
prompt: (prompt) => session.prompt(restate(prompt))
|
|
1785
|
+
});
|
|
1786
|
+
catalog.invalidate();
|
|
1787
|
+
await catalog.refresh(session.signal, true, false);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
ctx.on("credentials/record-updated", (key) => {
|
|
1792
|
+
if (key === RECORD_KEY) {
|
|
1793
|
+
catalog.invalidate();
|
|
1794
|
+
registration.replace(["lmm"]);
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
mountBrowserAuth(ctx, RECORD_KEY);
|
|
1798
|
+
ctx.effect(() => () => {
|
|
1799
|
+
integration.dispose();
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
//#endregion
|
|
1803
|
+
export { DSH_CLIENT_ID, RECORD_KEY, apply, credentialStoreFrom, inject, lmmProfile, name };
|