pi-meta-oauth 0.7.0 → 0.7.2
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 -7
- package/extensions/meta.ts +32 -785
- package/package.json +9 -3
- package/src/meta/constants.ts +28 -0
- package/src/meta/environment.ts +17 -0
- package/src/meta/http.ts +83 -0
- package/src/meta/model-store.ts +183 -0
- package/src/meta/models.ts +355 -0
- package/src/meta/muse-policy.ts +32 -0
- package/src/meta/oauth.ts +382 -0
- package/src/meta/provider.ts +53 -0
- package/src/meta/request-policy.ts +96 -0
- package/src/meta/types.ts +33 -0
package/extensions/meta.ts
CHANGED
|
@@ -1,789 +1,36 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* in with {@link MUSE_USER_AGENT_ENV_VAR}; see {@link isMuseUserAgentEnabled}.
|
|
33
|
-
*/
|
|
34
|
-
export const MUSE_USER_AGENT =
|
|
35
|
-
"muse-build/1.3.0 (non-interactive; linux-x86_64; build ac7280f2aca67769d1455a8847bb502b617d50f6)";
|
|
36
|
-
/** Set to `1`/`true`/`yes` to send {@link MUSE_USER_AGENT} and expose Contributor `max`. */
|
|
37
|
-
export const MUSE_USER_AGENT_ENV_VAR = "META_MUSE_USER_AGENT";
|
|
38
|
-
/** Models whose `max` effort Meta accepts only with the Muse fingerprint. */
|
|
39
|
-
const MUSE_USER_AGENT_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
40
|
-
"muse-spark-1.3-contributor",
|
|
41
|
-
]);
|
|
42
|
-
const META_ENV_VAR = "META_API_KEY";
|
|
43
|
-
|
|
44
|
-
const DEVICE_AUTHORIZATION_URL = `${META_AUTH_BASE_URL}/oidc/device/authorization/`;
|
|
45
|
-
const DEVICE_TOKEN_URL = `${META_AUTH_BASE_URL}/oidc/device/token/`;
|
|
46
|
-
const API_KEY_MINT_URL = "https://api.meta.ai/muse-code/key";
|
|
47
|
-
const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
48
|
-
const API_KEY_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
49
|
-
|
|
50
|
-
export type MetaProviderModel = NonNullable<ProviderConfig["models"]>[number];
|
|
51
|
-
type Fetch = typeof fetch;
|
|
52
|
-
type Sleep = (milliseconds: number) => Promise<void>;
|
|
53
|
-
|
|
54
|
-
interface DeviceAuthorization {
|
|
55
|
-
device_code: string;
|
|
56
|
-
user_code: string;
|
|
57
|
-
verification_uri: string;
|
|
58
|
-
verification_uri_complete?: string;
|
|
59
|
-
expires_in?: number;
|
|
60
|
-
interval?: number;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface DeviceTokenGrant {
|
|
64
|
-
access_token: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface OAuthError {
|
|
68
|
-
error?: string;
|
|
69
|
-
error_description?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface MintResponse {
|
|
73
|
-
api_key?: string;
|
|
74
|
-
base_url?: string;
|
|
75
|
-
require_payment?: boolean;
|
|
76
|
-
action_url?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface CatalogResponse {
|
|
80
|
-
data?: MetaCatalogModel[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface MetaCatalogModel {
|
|
84
|
-
id?: string;
|
|
85
|
-
metadata?: {
|
|
86
|
-
"muse-code"?: {
|
|
87
|
-
name?: string;
|
|
88
|
-
is_hidden?: boolean;
|
|
89
|
-
reasoning?: boolean;
|
|
90
|
-
modalities?: { input?: string[] };
|
|
91
|
-
limit?: { context?: number; output?: number };
|
|
92
|
-
variants?: Record<string, { reasoningEffort?: string }>;
|
|
93
|
-
cost?: {
|
|
94
|
-
input?: string | number;
|
|
95
|
-
output?: string | number;
|
|
96
|
-
cached?: string | number;
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const FALLBACK_MODELS: MetaProviderModel[] = [
|
|
103
|
-
{
|
|
104
|
-
id: "muse-spark-1.3",
|
|
105
|
-
name: "Muse Spark 1.3",
|
|
106
|
-
reasoning: true,
|
|
107
|
-
thinkingLevelMap: {
|
|
108
|
-
off: null,
|
|
109
|
-
minimal: "minimal",
|
|
110
|
-
low: "low",
|
|
111
|
-
medium: "medium",
|
|
112
|
-
high: "high",
|
|
113
|
-
xhigh: "xhigh",
|
|
114
|
-
max: "max",
|
|
115
|
-
},
|
|
116
|
-
input: ["text", "image"],
|
|
117
|
-
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
118
|
-
contextWindow: 1_048_576,
|
|
119
|
-
maxTokens: 256_000,
|
|
120
|
-
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
id: "muse-spark-1.3-contributor",
|
|
124
|
-
name: "Muse Spark 1.3 Contributor",
|
|
125
|
-
reasoning: true,
|
|
126
|
-
thinkingLevelMap: {
|
|
127
|
-
off: null,
|
|
128
|
-
minimal: "minimal",
|
|
129
|
-
low: "low",
|
|
130
|
-
medium: "medium",
|
|
131
|
-
high: "high",
|
|
132
|
-
xhigh: "xhigh",
|
|
133
|
-
// Accepted only with the Muse User-Agent fingerprint (live-verified
|
|
134
|
-
// 2026-09-25). gateMuseMaxEffort() maps this to "max" when
|
|
135
|
-
// META_MUSE_USER_AGENT opts in; otherwise it stays unexposed.
|
|
136
|
-
max: null,
|
|
137
|
-
},
|
|
138
|
-
input: ["text", "image"],
|
|
139
|
-
cost: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
|
|
140
|
-
contextWindow: 1_048_576,
|
|
141
|
-
maxTokens: 256_000,
|
|
142
|
-
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
id: "muse-spark-1.2",
|
|
146
|
-
name: "Muse Spark 1.2",
|
|
147
|
-
reasoning: true,
|
|
148
|
-
thinkingLevelMap: {
|
|
149
|
-
off: null,
|
|
150
|
-
minimal: "minimal",
|
|
151
|
-
low: "low",
|
|
152
|
-
medium: "medium",
|
|
153
|
-
high: "high",
|
|
154
|
-
xhigh: "xhigh",
|
|
155
|
-
max: null,
|
|
156
|
-
},
|
|
157
|
-
input: ["text", "image"],
|
|
158
|
-
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
159
|
-
contextWindow: 1_048_576,
|
|
160
|
-
maxTokens: 256_000,
|
|
161
|
-
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
id: "muse-spark-1.2-contributor",
|
|
165
|
-
name: "Muse Spark 1.2 Contributor",
|
|
166
|
-
reasoning: true,
|
|
167
|
-
thinkingLevelMap: {
|
|
168
|
-
off: null,
|
|
169
|
-
minimal: "minimal",
|
|
170
|
-
low: "low",
|
|
171
|
-
medium: "medium",
|
|
172
|
-
high: "high",
|
|
173
|
-
xhigh: "xhigh",
|
|
174
|
-
max: null,
|
|
175
|
-
},
|
|
176
|
-
input: ["text", "image"],
|
|
177
|
-
cost: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
|
|
178
|
-
contextWindow: 1_048_576,
|
|
179
|
-
maxTokens: 256_000,
|
|
180
|
-
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
id: "muse-spark-1.1",
|
|
184
|
-
name: "Muse Spark 1.1",
|
|
185
|
-
reasoning: true,
|
|
186
|
-
thinkingLevelMap: {
|
|
187
|
-
off: null,
|
|
188
|
-
minimal: "minimal",
|
|
189
|
-
low: "low",
|
|
190
|
-
medium: "medium",
|
|
191
|
-
high: "high",
|
|
192
|
-
xhigh: "xhigh",
|
|
193
|
-
max: null,
|
|
194
|
-
},
|
|
195
|
-
input: ["text", "image"],
|
|
196
|
-
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
197
|
-
contextWindow: 1_048_576,
|
|
198
|
-
maxTokens: 256_000,
|
|
199
|
-
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
200
|
-
},
|
|
201
|
-
];
|
|
202
|
-
|
|
203
|
-
type MetaEnv = Record<string, string | undefined>;
|
|
204
|
-
|
|
205
|
-
/** True when the user opted in to the Muse User-Agent fingerprint. */
|
|
206
|
-
export function isMuseUserAgentEnabled(env: MetaEnv = process.env): boolean {
|
|
207
|
-
const value = env[MUSE_USER_AGENT_ENV_VAR]?.trim().toLowerCase();
|
|
208
|
-
return value === "1" || value === "true" || value === "yes";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Expose `max` on fingerprint-gated models only when the fingerprint is
|
|
213
|
-
* enabled, so users who have not opted in are never offered a level that
|
|
214
|
-
* 400s. Any other explicit `max` mapping (e.g. a future server-advertised
|
|
215
|
-
* effort name) passes through unchanged.
|
|
216
|
-
*/
|
|
217
|
-
function gateMuseMaxEffort(
|
|
218
|
-
model: MetaProviderModel,
|
|
219
|
-
env: MetaEnv = process.env,
|
|
220
|
-
): MetaProviderModel {
|
|
221
|
-
const map = model.thinkingLevelMap;
|
|
222
|
-
if (!map || !MUSE_USER_AGENT_MODEL_IDS.has(model.id)) return model;
|
|
223
|
-
if (map.max != null && map.max !== "max") return model;
|
|
224
|
-
return {
|
|
225
|
-
...model,
|
|
226
|
-
thinkingLevelMap: { ...map, max: isMuseUserAgentEnabled(env) ? "max" : null },
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function fallbackModels(): MetaProviderModel[] {
|
|
231
|
-
return FALLBACK_MODELS.map((model) => gateMuseMaxEffort(model));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function delay(milliseconds: number): Promise<void> {
|
|
235
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function responseBody(
|
|
239
|
-
response: Response,
|
|
240
|
-
): Promise<Record<string, unknown>> {
|
|
241
|
-
const text = await response.text();
|
|
242
|
-
if (!text) return {};
|
|
243
|
-
try {
|
|
244
|
-
const value = JSON.parse(text) as unknown;
|
|
245
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
246
|
-
? (value as Record<string, unknown>)
|
|
247
|
-
: {};
|
|
248
|
-
} catch {
|
|
249
|
-
return {};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function errorDetail(body: Record<string, unknown>): string | undefined {
|
|
254
|
-
for (const key of ["error_description", "detail", "message", "error"]) {
|
|
255
|
-
const value = body[key];
|
|
256
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
257
|
-
}
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function postForm<T>(
|
|
262
|
-
url: string,
|
|
263
|
-
fields: Record<string, string>,
|
|
264
|
-
fetchImpl: Fetch,
|
|
265
|
-
): Promise<{ response: Response; body: T & Record<string, unknown> }> {
|
|
266
|
-
const response = await fetchImpl(url, {
|
|
267
|
-
method: "POST",
|
|
268
|
-
headers: {
|
|
269
|
-
Accept: "application/json",
|
|
270
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
271
|
-
},
|
|
272
|
-
body: new URLSearchParams(fields),
|
|
273
|
-
redirect: "manual",
|
|
274
|
-
});
|
|
275
|
-
return {
|
|
276
|
-
response,
|
|
277
|
-
body: (await responseBody(response)) as T & Record<string, unknown>,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function isAbortSignal(value: unknown): value is AbortSignal {
|
|
282
|
-
return (
|
|
283
|
-
typeof value === "object" &&
|
|
284
|
-
value !== null &&
|
|
285
|
-
"aborted" in value &&
|
|
286
|
-
typeof (value as AbortSignal).aborted === "boolean"
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export async function mintMetaApiKey(
|
|
291
|
-
identityToken: string,
|
|
292
|
-
fetchImpl: Fetch = fetch,
|
|
293
|
-
signal?: AbortSignal,
|
|
294
|
-
): Promise<string> {
|
|
295
|
-
const response = await fetchImpl(API_KEY_MINT_URL, {
|
|
296
|
-
method: "POST",
|
|
297
|
-
headers: {
|
|
298
|
-
Accept: "application/json",
|
|
299
|
-
Authorization: `Bearer ${identityToken}`,
|
|
300
|
-
"Content-Type": "application/json",
|
|
301
|
-
"x-api-version": "1.0.0",
|
|
302
|
-
},
|
|
303
|
-
body: "{}",
|
|
304
|
-
signal,
|
|
305
|
-
});
|
|
306
|
-
const body = (await responseBody(response)) as MintResponse &
|
|
307
|
-
Record<string, unknown>;
|
|
308
|
-
if (!response.ok) {
|
|
309
|
-
const detail = errorDetail(body);
|
|
310
|
-
if (response.status === 401 || response.status === 403) {
|
|
311
|
-
throw new Error(
|
|
312
|
-
`Meta session expired (HTTP ${response.status}); run /login meta again${detail ? `: ${detail}` : ""}`,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Meta API-key mint failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
if (typeof body.api_key !== "string" || !body.api_key) {
|
|
320
|
-
const setup =
|
|
321
|
-
typeof body.action_url === "string" && body.action_url
|
|
322
|
-
? ` Complete setup at ${body.action_url}.`
|
|
323
|
-
: "";
|
|
324
|
-
throw new Error(`Meta did not issue an API key.${setup}`);
|
|
325
|
-
}
|
|
326
|
-
return body.api_key;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export async function loginMeta(
|
|
330
|
-
callbacks: OAuthLoginCallbacks,
|
|
331
|
-
fetchImpl: Fetch = fetch,
|
|
332
|
-
sleep: Sleep = delay,
|
|
333
|
-
): Promise<OAuthCredentials> {
|
|
334
|
-
callbacks.onProgress?.("Starting Meta device authorization…");
|
|
335
|
-
const authorization = await postForm<DeviceAuthorization>(
|
|
336
|
-
DEVICE_AUTHORIZATION_URL,
|
|
337
|
-
{ client_id: META_CLIENT_ID },
|
|
338
|
-
fetchImpl,
|
|
339
|
-
);
|
|
340
|
-
if (!authorization.response.ok) {
|
|
341
|
-
throw new Error(
|
|
342
|
-
`Meta login could not be started (HTTP ${authorization.response.status})${errorDetail(authorization.body) ? `: ${errorDetail(authorization.body)}` : ""}`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
const device = authorization.body;
|
|
346
|
-
if (!device.device_code || !device.user_code || !device.verification_uri) {
|
|
347
|
-
throw new Error("Meta device authorization returned an incomplete response");
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
let intervalSeconds =
|
|
351
|
-
Number.isFinite(device.interval) && Number(device.interval) > 0
|
|
352
|
-
? Number(device.interval)
|
|
353
|
-
: 5;
|
|
354
|
-
const expiresInSeconds =
|
|
355
|
-
Number.isFinite(device.expires_in) && Number(device.expires_in) > 0
|
|
356
|
-
? Number(device.expires_in)
|
|
357
|
-
: 900;
|
|
358
|
-
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
359
|
-
callbacks.onDeviceCode({
|
|
360
|
-
userCode: device.user_code,
|
|
361
|
-
verificationUri: device.verification_uri_complete || device.verification_uri,
|
|
362
|
-
intervalSeconds,
|
|
363
|
-
expiresInSeconds,
|
|
364
|
-
});
|
|
365
|
-
callbacks.onProgress?.("Waiting for Meta login approval…");
|
|
366
|
-
|
|
367
|
-
let identityToken: string | undefined;
|
|
368
|
-
while (Date.now() < deadline) {
|
|
369
|
-
await sleep(intervalSeconds * 1000);
|
|
370
|
-
const grant = await postForm<DeviceTokenGrant & OAuthError>(
|
|
371
|
-
DEVICE_TOKEN_URL,
|
|
372
|
-
{
|
|
373
|
-
grant_type: DEVICE_CODE_GRANT,
|
|
374
|
-
device_code: device.device_code,
|
|
375
|
-
client_id: META_CLIENT_ID,
|
|
376
|
-
},
|
|
377
|
-
fetchImpl,
|
|
378
|
-
);
|
|
379
|
-
if (grant.response.ok && grant.body.access_token) {
|
|
380
|
-
identityToken = grant.body.access_token;
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
383
|
-
switch (grant.body.error) {
|
|
384
|
-
case "authorization_pending":
|
|
385
|
-
continue;
|
|
386
|
-
case "slow_down":
|
|
387
|
-
intervalSeconds += 5;
|
|
388
|
-
continue;
|
|
389
|
-
case "access_denied":
|
|
390
|
-
throw new Error("Meta login was denied");
|
|
391
|
-
case "expired_token":
|
|
392
|
-
throw new Error("Meta login request expired");
|
|
393
|
-
default:
|
|
394
|
-
throw new Error(
|
|
395
|
-
`Meta login failed (HTTP ${grant.response.status})${errorDetail(grant.body) ? `: ${errorDetail(grant.body)}` : ""}`,
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (!identityToken) throw new Error("Meta login request expired");
|
|
400
|
-
|
|
401
|
-
callbacks.onProgress?.("Enabling Meta Model API access…");
|
|
402
|
-
const apiKey = await mintMetaApiKey(identityToken, fetchImpl);
|
|
403
|
-
return {
|
|
404
|
-
refresh: identityToken,
|
|
405
|
-
access: apiKey,
|
|
406
|
-
expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export async function refreshMetaToken(
|
|
411
|
-
credentials: OAuthCredentials,
|
|
412
|
-
fetchOrSignal: Fetch | AbortSignal = fetch,
|
|
413
|
-
): Promise<OAuthCredentials> {
|
|
414
|
-
if (!credentials.refresh)
|
|
415
|
-
throw new Error(
|
|
416
|
-
"Meta login is missing its identity token; run /login meta again",
|
|
417
|
-
);
|
|
418
|
-
// Pi 0.83 calls refreshToken(credentials). Pi 0.84 passes AbortSignal as
|
|
419
|
-
// the second argument. Tests inject a fetch mock in that slot.
|
|
420
|
-
const fetchImpl = typeof fetchOrSignal === "function" ? fetchOrSignal : fetch;
|
|
421
|
-
const signal = isAbortSignal(fetchOrSignal) ? fetchOrSignal : undefined;
|
|
422
|
-
if (signal?.aborted) {
|
|
423
|
-
throw new Error("Meta token refresh was cancelled");
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
...credentials,
|
|
427
|
-
access: await mintMetaApiKey(credentials.refresh, fetchImpl, signal),
|
|
428
|
-
expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function finitePositive(value: unknown, fallback: number): number {
|
|
433
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
434
|
-
? value
|
|
435
|
-
: fallback;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function numericCost(value: unknown, fallback: number): number {
|
|
439
|
-
const number =
|
|
440
|
-
typeof value === "number"
|
|
441
|
-
? value
|
|
442
|
-
: typeof value === "string" && value.trim()
|
|
443
|
-
? Number(value)
|
|
444
|
-
: Number.NaN;
|
|
445
|
-
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function displayName(id: string): string {
|
|
449
|
-
return id
|
|
450
|
-
.split("-")
|
|
451
|
-
.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
|
|
452
|
-
.join(" ");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function modalitiesToInput(
|
|
456
|
-
modalities: string[] | undefined,
|
|
457
|
-
fallback: MetaProviderModel["input"] | undefined,
|
|
458
|
-
): MetaProviderModel["input"] {
|
|
459
|
-
if (!modalities) return fallback ?? ["text"];
|
|
460
|
-
const input: MetaProviderModel["input"] = ["text"];
|
|
461
|
-
if (modalities.includes("image")) input.push("image");
|
|
462
|
-
return input;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function toProviderModels(
|
|
466
|
-
catalog: CatalogResponse,
|
|
467
|
-
): MetaProviderModel[] {
|
|
468
|
-
return (catalog.data ?? []).flatMap((entry) => {
|
|
469
|
-
if (typeof entry.id !== "string" || !entry.id) return [];
|
|
470
|
-
const metadata = entry.metadata?.["muse-code"];
|
|
471
|
-
if (metadata?.is_hidden) return [];
|
|
472
|
-
// Gated fallbacks: a server-advertised variants.max still wins below.
|
|
473
|
-
const fallback = fallbackModels().find((model) => model.id === entry.id);
|
|
474
|
-
const catalogName = metadata?.name === entry.id ? undefined : metadata?.name;
|
|
475
|
-
const variants = metadata?.variants ?? {};
|
|
476
|
-
const thinkingLevelMap: NonNullable<MetaProviderModel["thinkingLevelMap"]> = {
|
|
477
|
-
off: null,
|
|
478
|
-
minimal: variants.minimal?.reasoningEffort ?? "minimal",
|
|
479
|
-
low: variants.low?.reasoningEffort ?? "low",
|
|
480
|
-
medium: variants.medium?.reasoningEffort ?? "medium",
|
|
481
|
-
high: variants.high?.reasoningEffort ?? "high",
|
|
482
|
-
xhigh: variants.xhigh?.reasoningEffort ?? "xhigh",
|
|
483
|
-
max: variants.max?.reasoningEffort ?? fallback?.thinkingLevelMap?.max ?? null,
|
|
484
|
-
};
|
|
485
|
-
return [
|
|
486
|
-
{
|
|
487
|
-
id: entry.id,
|
|
488
|
-
name: catalogName || fallback?.name || displayName(entry.id),
|
|
489
|
-
reasoning: metadata?.reasoning ?? fallback?.reasoning ?? true,
|
|
490
|
-
thinkingLevelMap,
|
|
491
|
-
input: modalitiesToInput(metadata?.modalities?.input, fallback?.input),
|
|
492
|
-
cost: {
|
|
493
|
-
input: numericCost(metadata?.cost?.input, fallback?.cost.input ?? 0),
|
|
494
|
-
output: numericCost(metadata?.cost?.output, fallback?.cost.output ?? 0),
|
|
495
|
-
cacheRead: numericCost(
|
|
496
|
-
metadata?.cost?.cached,
|
|
497
|
-
fallback?.cost.cacheRead ?? 0,
|
|
498
|
-
),
|
|
499
|
-
cacheWrite: 0,
|
|
500
|
-
},
|
|
501
|
-
contextWindow: finitePositive(
|
|
502
|
-
metadata?.limit?.context,
|
|
503
|
-
fallback?.contextWindow ?? 1_048_576,
|
|
504
|
-
),
|
|
505
|
-
maxTokens: finitePositive(
|
|
506
|
-
metadata?.limit?.output,
|
|
507
|
-
fallback?.maxTokens ?? 256_000,
|
|
508
|
-
),
|
|
509
|
-
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
510
|
-
} satisfies MetaProviderModel,
|
|
511
|
-
];
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
interface CatalogStore {
|
|
516
|
-
read(): Promise<ModelsStoreEntry | undefined>;
|
|
517
|
-
write(entry: ModelsStoreEntry): Promise<void>;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
interface CompatibleRefreshContext {
|
|
521
|
-
credential?: RefreshModelsContext["credential"];
|
|
522
|
-
allowNetwork: boolean;
|
|
523
|
-
signal?: AbortSignal;
|
|
524
|
-
// Pi 0.83 catalog persistence API.
|
|
525
|
-
store?: CatalogStore;
|
|
526
|
-
// Pi 0.84 generation-checked catalog persistence API.
|
|
527
|
-
stored?: Readonly<ModelsStoreEntry>;
|
|
528
|
-
publish?(publication: { persist?: ModelsStoreEntry | null }): Promise<boolean>;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function providerModelsFromStore(
|
|
532
|
-
entry: Readonly<ModelsStoreEntry> | undefined,
|
|
533
|
-
): MetaProviderModel[] {
|
|
534
|
-
return (entry?.models ?? []).flatMap((model: Model<Api>) => {
|
|
535
|
-
if (model.provider !== META_PROVIDER_ID || model.api !== "openai-responses")
|
|
536
|
-
return [];
|
|
537
|
-
// Re-gate on restore: a catalog cached while the fingerprint was enabled
|
|
538
|
-
// must not keep offering Contributor `max` after the user opts out.
|
|
539
|
-
return [
|
|
540
|
-
gateMuseMaxEffort({
|
|
541
|
-
id: model.id,
|
|
542
|
-
name: model.name,
|
|
543
|
-
api: model.api,
|
|
544
|
-
baseUrl: model.baseUrl,
|
|
545
|
-
reasoning: model.reasoning,
|
|
546
|
-
thinkingLevelMap: model.thinkingLevelMap,
|
|
547
|
-
input: model.input as MetaProviderModel["input"],
|
|
548
|
-
cost: model.cost,
|
|
549
|
-
contextWindow: model.contextWindow,
|
|
550
|
-
maxTokens: model.maxTokens,
|
|
551
|
-
headers: model.headers,
|
|
552
|
-
compat: model.compat as MetaProviderModel["compat"],
|
|
553
|
-
}),
|
|
554
|
-
];
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function modelsForStore(
|
|
559
|
-
models: MetaProviderModel[],
|
|
560
|
-
): Model<"openai-responses">[] {
|
|
561
|
-
return models.map((model) => ({
|
|
562
|
-
...model,
|
|
563
|
-
api: "openai-responses",
|
|
564
|
-
provider: META_PROVIDER_ID,
|
|
565
|
-
baseUrl: model.baseUrl ?? META_API_BASE_URL,
|
|
566
|
-
input: model.input as Model<"openai-responses">["input"],
|
|
567
|
-
compat: model.compat as Model<"openai-responses">["compat"],
|
|
568
|
-
}));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async function cachedMetaModels(
|
|
572
|
-
context: CompatibleRefreshContext,
|
|
573
|
-
): Promise<MetaProviderModel[]> {
|
|
574
|
-
try {
|
|
575
|
-
const stored = context.stored ?? (await context.store?.read());
|
|
576
|
-
return providerModelsFromStore(stored);
|
|
577
|
-
} catch {
|
|
578
|
-
// Catalog persistence is best-effort; bundled fallbacks remain available.
|
|
579
|
-
return [];
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async function persistMetaModels(
|
|
584
|
-
context: CompatibleRefreshContext,
|
|
585
|
-
entry: ModelsStoreEntry,
|
|
586
|
-
): Promise<void> {
|
|
587
|
-
if (context.publish) {
|
|
588
|
-
await context.publish({ persist: entry });
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
await context.store?.write(entry);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export async function refreshMetaModels(
|
|
595
|
-
context: RefreshModelsContext,
|
|
596
|
-
fetchImpl: Fetch = fetch,
|
|
597
|
-
): Promise<MetaProviderModel[]> {
|
|
598
|
-
// SAFETY: CompatibleRefreshContext is the union of the Pi 0.83 and 0.84
|
|
599
|
-
// refresh-context fields that this adapter probes defensively at runtime.
|
|
600
|
-
const compatibleContext = context as unknown as CompatibleRefreshContext;
|
|
601
|
-
if (!context.allowNetwork || context.signal?.aborted) {
|
|
602
|
-
const cached = await cachedMetaModels(compatibleContext);
|
|
603
|
-
return cached.length > 0 ? cached : fallbackModels();
|
|
604
|
-
}
|
|
605
|
-
const apiKey =
|
|
606
|
-
context.credential?.type === "oauth"
|
|
607
|
-
? context.credential.access
|
|
608
|
-
: context.credential?.type === "api_key"
|
|
609
|
-
? context.credential.key
|
|
610
|
-
: undefined;
|
|
611
|
-
if (!apiKey) {
|
|
612
|
-
const cached = await cachedMetaModels(compatibleContext);
|
|
613
|
-
return cached.length > 0 ? cached : fallbackModels();
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
const response = await fetchImpl(META_MODEL_CATALOG_URL, {
|
|
618
|
-
headers: {
|
|
619
|
-
Accept: "application/json",
|
|
620
|
-
Authorization: `Bearer ${apiKey}`,
|
|
621
|
-
"x-api-version": "1.0.0",
|
|
622
|
-
},
|
|
623
|
-
signal: context.signal,
|
|
624
|
-
});
|
|
625
|
-
const body = (await responseBody(response)) as CatalogResponse &
|
|
626
|
-
Record<string, unknown>;
|
|
627
|
-
if (!response.ok) {
|
|
628
|
-
throw new Error(
|
|
629
|
-
`Meta model catalog failed (HTTP ${response.status})${errorDetail(body) ? `: ${errorDetail(body)}` : ""}`,
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
const models = toProviderModels(body);
|
|
633
|
-
if (models.length === 0) {
|
|
634
|
-
const cached = await cachedMetaModels(compatibleContext);
|
|
635
|
-
return cached.length > 0 ? cached : fallbackModels();
|
|
636
|
-
}
|
|
637
|
-
if (!context.signal?.aborted) {
|
|
638
|
-
try {
|
|
639
|
-
await persistMetaModels(compatibleContext, {
|
|
640
|
-
models: modelsForStore(models),
|
|
641
|
-
checkedAt: Date.now(),
|
|
642
|
-
});
|
|
643
|
-
} catch {
|
|
644
|
-
// Keep the fresh catalog usable even if persistence fails.
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return models;
|
|
648
|
-
} catch (error) {
|
|
649
|
-
if (context.signal?.aborted) throw error;
|
|
650
|
-
const cached = await cachedMetaModels(compatibleContext);
|
|
651
|
-
return cached.length > 0 ? cached : fallbackModels();
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
export function metaFallbackCost(
|
|
656
|
-
modelId: string,
|
|
657
|
-
): MetaProviderModel["cost"] | undefined {
|
|
658
|
-
return FALLBACK_MODELS.find((model) => model.id === modelId)?.cost;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/** Meta prompt-cache opt-in. Measured 0% hits on /chat/completions vs 93–99% on /responses with 24h. */
|
|
662
|
-
export const META_PROMPT_CACHE_RETENTION = "24h";
|
|
663
|
-
|
|
664
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
665
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
666
|
-
? (value as Record<string, unknown>)
|
|
667
|
-
: undefined;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Strict direct-endpoint check: https scheme, api.meta.ai hostname
|
|
672
|
-
* (case-insensitive), default port, and a bare /v1 path. WHATWG URL parsing
|
|
673
|
-
* normalizes the equivalent forms (uppercase host, explicit :443) to match;
|
|
674
|
-
* proxies, lookalike hosts (api.meta.ai.evil.com), subpaths, and
|
|
675
|
-
* non-default ports never match, so they never receive the fingerprint.
|
|
676
|
-
*/
|
|
677
|
-
export function isDirectMetaModelApiUrl(baseUrl: unknown): boolean {
|
|
678
|
-
if (typeof baseUrl !== "string" || !baseUrl) return false;
|
|
679
|
-
let parsed: URL;
|
|
680
|
-
try {
|
|
681
|
-
parsed = new URL(baseUrl);
|
|
682
|
-
} catch {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
if (parsed.protocol !== "https:") return false;
|
|
686
|
-
if (parsed.hostname.toLowerCase() !== "api.meta.ai") return false;
|
|
687
|
-
if (parsed.port !== "") return false;
|
|
688
|
-
return parsed.pathname.replace(/\/+$/, "") === "/v1";
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function hasUserAgentHeader(
|
|
692
|
-
headers: Record<string, string | null>,
|
|
693
|
-
): boolean {
|
|
694
|
-
return Object.keys(headers).some(
|
|
695
|
-
(name) => name.toLowerCase() === "user-agent",
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Setdefault the Muse fingerprint for direct Meta Model API requests.
|
|
701
|
-
* Explicit User-Agent headers (any casing, including null suppression)
|
|
702
|
-
* always win; anything but the direct endpoint is left untouched.
|
|
703
|
-
* Applies regardless of credential type: API-key and Muse Code login
|
|
704
|
-
* (minted) credentials both reach the same direct wire. Returns true when
|
|
705
|
-
* the fingerprint was applied.
|
|
706
|
-
*/
|
|
707
|
-
export function applyMetaUserAgentFingerprint(
|
|
708
|
-
headers: Record<string, string | null>,
|
|
709
|
-
baseUrl: unknown,
|
|
710
|
-
): boolean {
|
|
711
|
-
if (!isDirectMetaModelApiUrl(baseUrl)) return false;
|
|
712
|
-
if (hasUserAgentHeader(headers)) return false;
|
|
713
|
-
headers["User-Agent"] = MUSE_USER_AGENT;
|
|
714
|
-
return true;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Hermes-equivalent Responses hints for api.meta.ai:
|
|
719
|
-
* setdefault `prompt_cache_retention: 24h`, and drop `reasoning.effort: none`
|
|
720
|
-
* because Meta 400s on it.
|
|
721
|
-
*/
|
|
722
|
-
export function applyMetaResponsesCacheHints(
|
|
723
|
-
payload: unknown,
|
|
724
|
-
): Record<string, unknown> | undefined {
|
|
725
|
-
const body = asRecord(payload);
|
|
726
|
-
if (!body) return undefined;
|
|
727
|
-
if (body.prompt_cache_retention === undefined) {
|
|
728
|
-
body.prompt_cache_retention = META_PROMPT_CACHE_RETENTION;
|
|
729
|
-
}
|
|
730
|
-
const reasoning = asRecord(body.reasoning);
|
|
731
|
-
if (
|
|
732
|
-
reasoning &&
|
|
733
|
-
(reasoning.effort === "none" ||
|
|
734
|
-
reasoning.effort === undefined ||
|
|
735
|
-
reasoning.effort === null)
|
|
736
|
-
) {
|
|
737
|
-
delete body.reasoning;
|
|
738
|
-
}
|
|
739
|
-
return body;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
export function createMetaProviderConfig(): ProviderConfig {
|
|
743
|
-
return {
|
|
744
|
-
name: "Meta Model API",
|
|
745
|
-
baseUrl: META_API_BASE_URL,
|
|
746
|
-
api: "openai-responses",
|
|
747
|
-
apiKey: "$META_API_KEY",
|
|
748
|
-
models: fallbackModels(),
|
|
749
|
-
refreshModels: refreshMetaModels,
|
|
750
|
-
oauth: {
|
|
751
|
-
name: "Meta Model API (browser login)",
|
|
752
|
-
login: loginMeta,
|
|
753
|
-
refreshToken: refreshMetaToken,
|
|
754
|
-
getApiKey: (credentials: { access: string }) => credentials.access,
|
|
755
|
-
},
|
|
756
|
-
};
|
|
757
|
-
}
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { META_PROVIDER_ID } from "../src/meta/constants.ts";
|
|
3
|
+
import { synchronizeMetaApiKeyAliases } from "../src/meta/environment.ts";
|
|
4
|
+
import { createMetaProviderConfig } from "../src/meta/provider.ts";
|
|
5
|
+
|
|
6
|
+
// Preserve the extension's existing named exports for callers and wire tests.
|
|
7
|
+
export {
|
|
8
|
+
META_API_BASE_URL,
|
|
9
|
+
META_AUTH_BASE_URL,
|
|
10
|
+
META_CLIENT_ID,
|
|
11
|
+
META_MODEL_CATALOG_URL,
|
|
12
|
+
META_PROMPT_CACHE_RETENTION,
|
|
13
|
+
META_PROVIDER_ID,
|
|
14
|
+
MUSE_USER_AGENT,
|
|
15
|
+
MUSE_USER_AGENT_ENV_VAR,
|
|
16
|
+
} from "../src/meta/constants.ts";
|
|
17
|
+
export { refreshMetaModels } from "../src/meta/model-store.ts";
|
|
18
|
+
export { metaFallbackCost, toProviderModels } from "../src/meta/models.ts";
|
|
19
|
+
export { isMuseUserAgentEnabled } from "../src/meta/muse-policy.ts";
|
|
20
|
+
export {
|
|
21
|
+
loginMeta,
|
|
22
|
+
mintMetaApiKey,
|
|
23
|
+
refreshMetaToken,
|
|
24
|
+
} from "../src/meta/oauth.ts";
|
|
25
|
+
export { createMetaProviderConfig } from "../src/meta/provider.ts";
|
|
26
|
+
export {
|
|
27
|
+
applyMetaResponsesCacheHints,
|
|
28
|
+
applyMetaUserAgentFingerprint,
|
|
29
|
+
isDirectMetaModelApiUrl,
|
|
30
|
+
} from "../src/meta/request-policy.ts";
|
|
31
|
+
export type { MetaProviderModel } from "../src/meta/types.ts";
|
|
758
32
|
|
|
759
33
|
export default function metaOAuthProvider(pi: ExtensionAPI): void {
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
process.env[META_ENV_VAR] === undefined &&
|
|
763
|
-
process.env["MODEL_API_KEY"] !== undefined
|
|
764
|
-
) {
|
|
765
|
-
process.env[META_ENV_VAR] = process.env["MODEL_API_KEY"];
|
|
766
|
-
}
|
|
767
|
-
if (
|
|
768
|
-
process.env["MODEL_API_KEY"] === undefined &&
|
|
769
|
-
process.env[META_ENV_VAR] !== undefined
|
|
770
|
-
) {
|
|
771
|
-
process.env["MODEL_API_KEY"] = process.env[META_ENV_VAR];
|
|
772
|
-
}
|
|
34
|
+
synchronizeMetaApiKeyAliases(process.env);
|
|
773
35
|
pi.registerProvider(META_PROVIDER_ID, createMetaProviderConfig());
|
|
774
|
-
pi.on("before_provider_request", (event, ctx) => {
|
|
775
|
-
if (ctx.model?.provider !== META_PROVIDER_ID) return undefined;
|
|
776
|
-
return applyMetaResponsesCacheHints(event.payload);
|
|
777
|
-
});
|
|
778
|
-
pi.on("before_provider_headers", (event, ctx) => {
|
|
779
|
-
const model = ctx.model;
|
|
780
|
-
if (model?.provider !== META_PROVIDER_ID) return;
|
|
781
|
-
// Opt-in only, and only for models that need it: every other Meta
|
|
782
|
-
// request keeps Pi's own User-Agent.
|
|
783
|
-
if (!MUSE_USER_AGENT_MODEL_IDS.has(model.id)) return;
|
|
784
|
-
if (!isMuseUserAgentEnabled()) return;
|
|
785
|
-
// The composed model always carries the effective baseUrl; without
|
|
786
|
-
// it the endpoint cannot be verified, so no fingerprint is sent.
|
|
787
|
-
applyMetaUserAgentFingerprint(event.headers, model.baseUrl);
|
|
788
|
-
});
|
|
789
36
|
}
|