@geocine/pi-meta-oauth 0.6.1
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/LICENSE +21 -0
- package/README.md +28 -0
- package/extensions/meta.ts +851 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BlockedPath
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# pi-meta-oauth
|
|
2
|
+
|
|
3
|
+
Meta Model API OAuth for [pi](https://pi.dev). Use Muse Spark models through Pi's `openai-responses` provider.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install git:github.com/geocine/pi-meta-oauth@geocine
|
|
9
|
+
pi --list-models meta
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Login
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
/login meta
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Pick browser login (device flow) or paste a Model API key. Device-flow keys are re-minted daily; pasted keys are stored as-is. Or skip login entirely with `META_API_KEY` / `MODEL_API_KEY`.
|
|
19
|
+
|
|
20
|
+
## Models
|
|
21
|
+
|
|
22
|
+
`muse-spark-1.3`, `muse-spark-1.3-contributor`, `muse-spark-1.2`, `muse-spark-1.2-contributor`, `muse-spark-1.1`
|
|
23
|
+
|
|
24
|
+
## Verify
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi -p --provider meta --model muse-spark-1.3 "Reply exactly: META_OK"
|
|
28
|
+
```
|
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
Model,
|
|
4
|
+
ModelsStoreEntry,
|
|
5
|
+
OAuthCredentials,
|
|
6
|
+
OAuthLoginCallbacks,
|
|
7
|
+
RefreshModelsContext,
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ProviderConfig,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
export const META_PROVIDER_ID = "meta";
|
|
15
|
+
export const META_API_BASE_URL = "https://api.meta.ai/v1";
|
|
16
|
+
export const META_MODEL_CATALOG_URL = "https://api.meta.ai/v1/models";
|
|
17
|
+
export const META_AUTH_BASE_URL = "https://auth.meta.com";
|
|
18
|
+
export const META_CLIENT_ID = "1031625952748946";
|
|
19
|
+
const META_ENV_VAR = "META_API_KEY";
|
|
20
|
+
|
|
21
|
+
const DEVICE_AUTHORIZATION_URL = `${META_AUTH_BASE_URL}/oidc/device/authorization/`;
|
|
22
|
+
const DEVICE_TOKEN_URL = `${META_AUTH_BASE_URL}/oidc/device/token/`;
|
|
23
|
+
const API_KEY_MINT_URL = "https://api.meta.ai/muse-code/key";
|
|
24
|
+
const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
25
|
+
const API_KEY_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Marks credentials created by pasting a Model API key instead of the device
|
|
29
|
+
* flow. The marker lives in `refresh` (OAuthCredentials requires one), so
|
|
30
|
+
* refreshMetaToken can tell a static key from an identity token and must not
|
|
31
|
+
* send it to the mint endpoint.
|
|
32
|
+
*/
|
|
33
|
+
export const STATIC_API_KEY_PREFIX = "static-api-key:";
|
|
34
|
+
|
|
35
|
+
export type MetaProviderModel = NonNullable<ProviderConfig["models"]>[number];
|
|
36
|
+
type Fetch = typeof fetch;
|
|
37
|
+
type Sleep = (milliseconds: number) => Promise<void>;
|
|
38
|
+
|
|
39
|
+
interface DeviceAuthorization {
|
|
40
|
+
device_code: string;
|
|
41
|
+
user_code: string;
|
|
42
|
+
verification_uri: string;
|
|
43
|
+
verification_uri_complete?: string;
|
|
44
|
+
expires_in?: number;
|
|
45
|
+
interval?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface DeviceTokenGrant {
|
|
49
|
+
access_token: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface OAuthError {
|
|
53
|
+
error?: string;
|
|
54
|
+
error_description?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface MintResponse {
|
|
58
|
+
api_key?: string;
|
|
59
|
+
base_url?: string;
|
|
60
|
+
require_payment?: boolean;
|
|
61
|
+
action_url?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface CatalogResponse {
|
|
65
|
+
data?: MetaCatalogModel[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface MetaCatalogModel {
|
|
69
|
+
id?: string;
|
|
70
|
+
metadata?: {
|
|
71
|
+
"muse-code"?: {
|
|
72
|
+
name?: string;
|
|
73
|
+
is_hidden?: boolean;
|
|
74
|
+
reasoning?: boolean;
|
|
75
|
+
modalities?: { input?: string[] };
|
|
76
|
+
limit?: { context?: number; output?: number };
|
|
77
|
+
variants?: Record<string, { reasoningEffort?: string }>;
|
|
78
|
+
cost?: {
|
|
79
|
+
input?: string | number;
|
|
80
|
+
output?: string | number;
|
|
81
|
+
cached?: string | number;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const FALLBACK_MODELS: MetaProviderModel[] = [
|
|
88
|
+
{
|
|
89
|
+
id: "muse-spark-1.3",
|
|
90
|
+
name: "Muse Spark 1.3",
|
|
91
|
+
reasoning: true,
|
|
92
|
+
thinkingLevelMap: {
|
|
93
|
+
off: null,
|
|
94
|
+
minimal: "minimal",
|
|
95
|
+
low: "low",
|
|
96
|
+
medium: "medium",
|
|
97
|
+
high: "high",
|
|
98
|
+
xhigh: "xhigh",
|
|
99
|
+
max: "max",
|
|
100
|
+
},
|
|
101
|
+
input: ["text", "image"],
|
|
102
|
+
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
103
|
+
contextWindow: 1_048_576,
|
|
104
|
+
maxTokens: 256_000,
|
|
105
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "muse-spark-1.3-contributor",
|
|
109
|
+
name: "Muse Spark 1.3 Contributor",
|
|
110
|
+
reasoning: true,
|
|
111
|
+
thinkingLevelMap: {
|
|
112
|
+
off: null,
|
|
113
|
+
minimal: "minimal",
|
|
114
|
+
low: "low",
|
|
115
|
+
medium: "medium",
|
|
116
|
+
high: "high",
|
|
117
|
+
xhigh: "xhigh",
|
|
118
|
+
max: null,
|
|
119
|
+
},
|
|
120
|
+
input: ["text", "image"],
|
|
121
|
+
cost: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
|
|
122
|
+
contextWindow: 1_048_576,
|
|
123
|
+
maxTokens: 256_000,
|
|
124
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "muse-spark-1.2",
|
|
128
|
+
name: "Muse Spark 1.2",
|
|
129
|
+
reasoning: true,
|
|
130
|
+
thinkingLevelMap: {
|
|
131
|
+
off: null,
|
|
132
|
+
minimal: "minimal",
|
|
133
|
+
low: "low",
|
|
134
|
+
medium: "medium",
|
|
135
|
+
high: "high",
|
|
136
|
+
xhigh: "xhigh",
|
|
137
|
+
max: null,
|
|
138
|
+
},
|
|
139
|
+
input: ["text", "image"],
|
|
140
|
+
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
141
|
+
contextWindow: 1_048_576,
|
|
142
|
+
maxTokens: 256_000,
|
|
143
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "muse-spark-1.2-contributor",
|
|
147
|
+
name: "Muse Spark 1.2 Contributor",
|
|
148
|
+
reasoning: true,
|
|
149
|
+
thinkingLevelMap: {
|
|
150
|
+
off: null,
|
|
151
|
+
minimal: "minimal",
|
|
152
|
+
low: "low",
|
|
153
|
+
medium: "medium",
|
|
154
|
+
high: "high",
|
|
155
|
+
xhigh: "xhigh",
|
|
156
|
+
max: null,
|
|
157
|
+
},
|
|
158
|
+
input: ["text", "image"],
|
|
159
|
+
cost: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
|
|
160
|
+
contextWindow: 1_048_576,
|
|
161
|
+
maxTokens: 256_000,
|
|
162
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "muse-spark-1.1",
|
|
166
|
+
name: "Muse Spark 1.1",
|
|
167
|
+
reasoning: true,
|
|
168
|
+
thinkingLevelMap: {
|
|
169
|
+
off: null,
|
|
170
|
+
minimal: "minimal",
|
|
171
|
+
low: "low",
|
|
172
|
+
medium: "medium",
|
|
173
|
+
high: "high",
|
|
174
|
+
xhigh: "xhigh",
|
|
175
|
+
max: null,
|
|
176
|
+
},
|
|
177
|
+
input: ["text", "image"],
|
|
178
|
+
cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
|
179
|
+
contextWindow: 1_048_576,
|
|
180
|
+
maxTokens: 256_000,
|
|
181
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
function delay(milliseconds: number): Promise<void> {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function responseBody(
|
|
190
|
+
response: Response,
|
|
191
|
+
): Promise<Record<string, unknown>> {
|
|
192
|
+
const text = await response.text();
|
|
193
|
+
if (!text) return {};
|
|
194
|
+
try {
|
|
195
|
+
const value = JSON.parse(text) as unknown;
|
|
196
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
197
|
+
? (value as Record<string, unknown>)
|
|
198
|
+
: {};
|
|
199
|
+
} catch {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function errorDetail(body: Record<string, unknown>): string | undefined {
|
|
205
|
+
for (const key of ["error_description", "detail", "message", "error"]) {
|
|
206
|
+
const value = body[key];
|
|
207
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function postForm<T>(
|
|
213
|
+
url: string,
|
|
214
|
+
fields: Record<string, string>,
|
|
215
|
+
fetchImpl: Fetch,
|
|
216
|
+
): Promise<{ response: Response; body: T & Record<string, unknown> }> {
|
|
217
|
+
const response = await fetchImpl(url, {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: {
|
|
220
|
+
Accept: "application/json",
|
|
221
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
222
|
+
},
|
|
223
|
+
body: new URLSearchParams(fields),
|
|
224
|
+
redirect: "manual",
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
response,
|
|
228
|
+
body: (await responseBody(response)) as T & Record<string, unknown>,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isAbortSignal(value: unknown): value is AbortSignal {
|
|
233
|
+
return (
|
|
234
|
+
typeof value === "object" &&
|
|
235
|
+
value !== null &&
|
|
236
|
+
"aborted" in value &&
|
|
237
|
+
typeof (value as AbortSignal).aborted === "boolean"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function mintMetaApiKey(
|
|
242
|
+
identityToken: string,
|
|
243
|
+
fetchImpl: Fetch = fetch,
|
|
244
|
+
signal?: AbortSignal,
|
|
245
|
+
): Promise<string> {
|
|
246
|
+
const response = await fetchImpl(API_KEY_MINT_URL, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: {
|
|
249
|
+
Accept: "application/json",
|
|
250
|
+
Authorization: `Bearer ${identityToken}`,
|
|
251
|
+
"Content-Type": "application/json",
|
|
252
|
+
"x-api-version": "1.0.0",
|
|
253
|
+
},
|
|
254
|
+
body: "{}",
|
|
255
|
+
signal,
|
|
256
|
+
});
|
|
257
|
+
const body = (await responseBody(response)) as MintResponse &
|
|
258
|
+
Record<string, unknown>;
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
const detail = errorDetail(body);
|
|
261
|
+
if (response.status === 401 || response.status === 403) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Meta session expired (HTTP ${response.status}); run /login meta again${detail ? `: ${detail}` : ""}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Meta API-key mint failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (typeof body.api_key !== "string" || !body.api_key) {
|
|
271
|
+
const setup =
|
|
272
|
+
typeof body.action_url === "string" && body.action_url
|
|
273
|
+
? ` Complete setup at ${body.action_url}.`
|
|
274
|
+
: "";
|
|
275
|
+
throw new Error(`Meta did not issue an API key.${setup}`);
|
|
276
|
+
}
|
|
277
|
+
return body.api_key;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* API-key login: prompt for a Meta Model API key, validate it against the
|
|
282
|
+
* model catalog, and store it as a static credential. No daily re-minting —
|
|
283
|
+
* refreshMetaToken passes the key through unchanged.
|
|
284
|
+
*/
|
|
285
|
+
export async function loginMetaWithApiKey(
|
|
286
|
+
callbacks: OAuthLoginCallbacks,
|
|
287
|
+
fetchImpl: Fetch = fetch,
|
|
288
|
+
): Promise<OAuthCredentials> {
|
|
289
|
+
const key = (
|
|
290
|
+
await callbacks.onPrompt({ message: "Meta Model API key:" })
|
|
291
|
+
).trim();
|
|
292
|
+
if (!key) throw new Error("Meta login requires an API key");
|
|
293
|
+
callbacks.onProgress?.("Validating Meta Model API key…");
|
|
294
|
+
const response = await fetchImpl(META_MODEL_CATALOG_URL, {
|
|
295
|
+
headers: {
|
|
296
|
+
Accept: "application/json",
|
|
297
|
+
Authorization: `Bearer ${key}`,
|
|
298
|
+
"x-api-version": "1.0.0",
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const detail = errorDetail(await responseBody(response));
|
|
303
|
+
if (response.status === 401 || response.status === 403) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Meta rejected the API key (HTTP ${response.status})${detail ? `: ${detail}` : ""}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Meta API key validation failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
refresh: `${STATIC_API_KEY_PREFIX}${key}`,
|
|
314
|
+
access: key,
|
|
315
|
+
expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function loginMeta(
|
|
320
|
+
callbacks: OAuthLoginCallbacks,
|
|
321
|
+
fetchImpl: Fetch = fetch,
|
|
322
|
+
sleep: Sleep = delay,
|
|
323
|
+
): Promise<OAuthCredentials> {
|
|
324
|
+
// Offer API-key login next to the device flow. Hosts without onSelect and
|
|
325
|
+
// dismissed selectors (undefined) keep the original device-flow behavior.
|
|
326
|
+
const method = await callbacks.onSelect?.({
|
|
327
|
+
message: "Select Meta login method:",
|
|
328
|
+
options: [
|
|
329
|
+
{ id: "browser", label: "Browser login (Meta device flow)" },
|
|
330
|
+
{ id: "api-key", label: "Paste a Model API key" },
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
if (method === "api-key") {
|
|
334
|
+
return loginMetaWithApiKey(callbacks, fetchImpl);
|
|
335
|
+
}
|
|
336
|
+
callbacks.onProgress?.("Starting Meta device authorization…");
|
|
337
|
+
const authorization = await postForm<DeviceAuthorization>(
|
|
338
|
+
DEVICE_AUTHORIZATION_URL,
|
|
339
|
+
{ client_id: META_CLIENT_ID },
|
|
340
|
+
fetchImpl,
|
|
341
|
+
);
|
|
342
|
+
if (!authorization.response.ok) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Meta login could not be started (HTTP ${authorization.response.status})${errorDetail(authorization.body) ? `: ${errorDetail(authorization.body)}` : ""}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const device = authorization.body;
|
|
348
|
+
if (!device.device_code || !device.user_code || !device.verification_uri) {
|
|
349
|
+
throw new Error("Meta device authorization returned an incomplete response");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let intervalSeconds =
|
|
353
|
+
Number.isFinite(device.interval) && Number(device.interval) > 0
|
|
354
|
+
? Number(device.interval)
|
|
355
|
+
: 5;
|
|
356
|
+
const expiresInSeconds =
|
|
357
|
+
Number.isFinite(device.expires_in) && Number(device.expires_in) > 0
|
|
358
|
+
? Number(device.expires_in)
|
|
359
|
+
: 900;
|
|
360
|
+
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
361
|
+
callbacks.onDeviceCode({
|
|
362
|
+
userCode: device.user_code,
|
|
363
|
+
verificationUri: device.verification_uri_complete || device.verification_uri,
|
|
364
|
+
intervalSeconds,
|
|
365
|
+
expiresInSeconds,
|
|
366
|
+
});
|
|
367
|
+
callbacks.onProgress?.("Waiting for Meta login approval…");
|
|
368
|
+
|
|
369
|
+
let identityToken: string | undefined;
|
|
370
|
+
while (Date.now() < deadline) {
|
|
371
|
+
await sleep(intervalSeconds * 1000);
|
|
372
|
+
const grant = await postForm<DeviceTokenGrant & OAuthError>(
|
|
373
|
+
DEVICE_TOKEN_URL,
|
|
374
|
+
{
|
|
375
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
376
|
+
device_code: device.device_code,
|
|
377
|
+
client_id: META_CLIENT_ID,
|
|
378
|
+
},
|
|
379
|
+
fetchImpl,
|
|
380
|
+
);
|
|
381
|
+
if (grant.response.ok && grant.body.access_token) {
|
|
382
|
+
identityToken = grant.body.access_token;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
switch (grant.body.error) {
|
|
386
|
+
case "authorization_pending":
|
|
387
|
+
continue;
|
|
388
|
+
case "slow_down":
|
|
389
|
+
intervalSeconds += 5;
|
|
390
|
+
continue;
|
|
391
|
+
case "access_denied":
|
|
392
|
+
throw new Error("Meta login was denied");
|
|
393
|
+
case "expired_token":
|
|
394
|
+
throw new Error("Meta login request expired");
|
|
395
|
+
default:
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Meta login failed (HTTP ${grant.response.status})${errorDetail(grant.body) ? `: ${errorDetail(grant.body)}` : ""}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!identityToken) throw new Error("Meta login request expired");
|
|
402
|
+
|
|
403
|
+
callbacks.onProgress?.("Enabling Meta Model API access…");
|
|
404
|
+
const apiKey = await mintMetaApiKey(identityToken, fetchImpl);
|
|
405
|
+
return {
|
|
406
|
+
refresh: identityToken,
|
|
407
|
+
access: apiKey,
|
|
408
|
+
expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function refreshMetaToken(
|
|
413
|
+
credentials: OAuthCredentials,
|
|
414
|
+
fetchOrSignal: Fetch | AbortSignal = fetch,
|
|
415
|
+
): Promise<OAuthCredentials> {
|
|
416
|
+
if (credentials.refresh?.startsWith(STATIC_API_KEY_PREFIX)) {
|
|
417
|
+
// Static API-key login: nothing to re-mint; keep the key, roll expiry.
|
|
418
|
+
return {
|
|
419
|
+
...credentials,
|
|
420
|
+
access:
|
|
421
|
+
credentials.refresh.slice(STATIC_API_KEY_PREFIX.length) ||
|
|
422
|
+
credentials.access,
|
|
423
|
+
expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (!credentials.refresh)
|
|
427
|
+
throw new Error(
|
|
428
|
+
"Meta login is missing its identity token; run /login meta again",
|
|
429
|
+
);
|
|
430
|
+
// Pi 0.83 calls refreshToken(credentials). Pi 0.84 passes AbortSignal as
|
|
431
|
+
// the second argument. Tests inject a fetch mock in that slot.
|
|
432
|
+
const fetchImpl = typeof fetchOrSignal === "function" ? fetchOrSignal : fetch;
|
|
433
|
+
const signal = isAbortSignal(fetchOrSignal) ? fetchOrSignal : undefined;
|
|
434
|
+
if (signal?.aborted) {
|
|
435
|
+
throw new Error("Meta token refresh was cancelled");
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
...credentials,
|
|
439
|
+
access: await mintMetaApiKey(credentials.refresh, fetchImpl, signal),
|
|
440
|
+
expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function finitePositive(value: unknown, fallback: number): number {
|
|
445
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
446
|
+
? value
|
|
447
|
+
: fallback;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function numericCost(value: unknown, fallback: number): number {
|
|
451
|
+
const number =
|
|
452
|
+
typeof value === "number"
|
|
453
|
+
? value
|
|
454
|
+
: typeof value === "string" && value.trim()
|
|
455
|
+
? Number(value)
|
|
456
|
+
: Number.NaN;
|
|
457
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function displayName(id: string): string {
|
|
461
|
+
return id
|
|
462
|
+
.split("-")
|
|
463
|
+
.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
|
|
464
|
+
.join(" ");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function modalitiesToInput(
|
|
468
|
+
modalities: string[] | undefined,
|
|
469
|
+
fallback: MetaProviderModel["input"] | undefined,
|
|
470
|
+
): MetaProviderModel["input"] {
|
|
471
|
+
if (!modalities) return fallback ?? ["text"];
|
|
472
|
+
const input: MetaProviderModel["input"] = ["text"];
|
|
473
|
+
if (modalities.includes("image")) input.push("image");
|
|
474
|
+
return input;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function toProviderModels(
|
|
478
|
+
catalog: CatalogResponse,
|
|
479
|
+
): MetaProviderModel[] {
|
|
480
|
+
return (catalog.data ?? []).flatMap((entry) => {
|
|
481
|
+
if (typeof entry.id !== "string" || !entry.id) return [];
|
|
482
|
+
const metadata = entry.metadata?.["muse-code"];
|
|
483
|
+
if (metadata?.is_hidden) return [];
|
|
484
|
+
const fallback = FALLBACK_MODELS.find((model) => model.id === entry.id);
|
|
485
|
+
const catalogName = metadata?.name === entry.id ? undefined : metadata?.name;
|
|
486
|
+
const variants = metadata?.variants ?? {};
|
|
487
|
+
const thinkingLevelMap: NonNullable<MetaProviderModel["thinkingLevelMap"]> = {
|
|
488
|
+
off: null,
|
|
489
|
+
minimal: variants.minimal?.reasoningEffort ?? "minimal",
|
|
490
|
+
low: variants.low?.reasoningEffort ?? "low",
|
|
491
|
+
medium: variants.medium?.reasoningEffort ?? "medium",
|
|
492
|
+
high: variants.high?.reasoningEffort ?? "high",
|
|
493
|
+
xhigh: variants.xhigh?.reasoningEffort ?? "xhigh",
|
|
494
|
+
max: variants.max?.reasoningEffort ?? fallback?.thinkingLevelMap?.max ?? null,
|
|
495
|
+
};
|
|
496
|
+
return [
|
|
497
|
+
{
|
|
498
|
+
id: entry.id,
|
|
499
|
+
name: catalogName || fallback?.name || displayName(entry.id),
|
|
500
|
+
reasoning: metadata?.reasoning ?? fallback?.reasoning ?? true,
|
|
501
|
+
thinkingLevelMap,
|
|
502
|
+
input: modalitiesToInput(metadata?.modalities?.input, fallback?.input),
|
|
503
|
+
cost: {
|
|
504
|
+
input: numericCost(metadata?.cost?.input, fallback?.cost.input ?? 0),
|
|
505
|
+
output: numericCost(metadata?.cost?.output, fallback?.cost.output ?? 0),
|
|
506
|
+
cacheRead: numericCost(
|
|
507
|
+
metadata?.cost?.cached,
|
|
508
|
+
fallback?.cost.cacheRead ?? 0,
|
|
509
|
+
),
|
|
510
|
+
cacheWrite: 0,
|
|
511
|
+
},
|
|
512
|
+
contextWindow: finitePositive(
|
|
513
|
+
metadata?.limit?.context,
|
|
514
|
+
fallback?.contextWindow ?? 1_048_576,
|
|
515
|
+
),
|
|
516
|
+
maxTokens: finitePositive(
|
|
517
|
+
metadata?.limit?.output,
|
|
518
|
+
fallback?.maxTokens ?? 256_000,
|
|
519
|
+
),
|
|
520
|
+
compat: { supportsReasoningEffort: true, supportsToolSearch: true },
|
|
521
|
+
} satisfies MetaProviderModel,
|
|
522
|
+
];
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
interface CatalogStore {
|
|
527
|
+
read(): Promise<ModelsStoreEntry | undefined>;
|
|
528
|
+
write(entry: ModelsStoreEntry): Promise<void>;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
interface CompatibleRefreshContext {
|
|
532
|
+
credential?: RefreshModelsContext["credential"];
|
|
533
|
+
allowNetwork: boolean;
|
|
534
|
+
signal?: AbortSignal;
|
|
535
|
+
// Pi 0.83 catalog persistence API.
|
|
536
|
+
store?: CatalogStore;
|
|
537
|
+
// Pi 0.84 generation-checked catalog persistence API.
|
|
538
|
+
stored?: Readonly<ModelsStoreEntry>;
|
|
539
|
+
publish?(publication: { persist?: ModelsStoreEntry | null }): Promise<boolean>;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function providerModelsFromStore(
|
|
543
|
+
entry: Readonly<ModelsStoreEntry> | undefined,
|
|
544
|
+
): MetaProviderModel[] {
|
|
545
|
+
return (entry?.models ?? []).flatMap((model: Model<Api>) => {
|
|
546
|
+
if (model.provider !== META_PROVIDER_ID || model.api !== "openai-responses")
|
|
547
|
+
return [];
|
|
548
|
+
return [
|
|
549
|
+
{
|
|
550
|
+
id: model.id,
|
|
551
|
+
name: model.name,
|
|
552
|
+
api: model.api,
|
|
553
|
+
baseUrl: model.baseUrl,
|
|
554
|
+
reasoning: model.reasoning,
|
|
555
|
+
thinkingLevelMap: model.thinkingLevelMap,
|
|
556
|
+
input: model.input as MetaProviderModel["input"],
|
|
557
|
+
cost: model.cost,
|
|
558
|
+
contextWindow: model.contextWindow,
|
|
559
|
+
maxTokens: model.maxTokens,
|
|
560
|
+
headers: model.headers,
|
|
561
|
+
compat: model.compat as MetaProviderModel["compat"],
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function modelsForStore(
|
|
568
|
+
models: MetaProviderModel[],
|
|
569
|
+
): Model<"openai-responses">[] {
|
|
570
|
+
return models.map((model) => ({
|
|
571
|
+
...model,
|
|
572
|
+
api: "openai-responses",
|
|
573
|
+
provider: META_PROVIDER_ID,
|
|
574
|
+
baseUrl: model.baseUrl ?? META_API_BASE_URL,
|
|
575
|
+
input: model.input as Model<"openai-responses">["input"],
|
|
576
|
+
compat: model.compat as Model<"openai-responses">["compat"],
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function cachedMetaModels(
|
|
581
|
+
context: CompatibleRefreshContext,
|
|
582
|
+
): Promise<MetaProviderModel[]> {
|
|
583
|
+
try {
|
|
584
|
+
const stored = context.stored ?? (await context.store?.read());
|
|
585
|
+
return providerModelsFromStore(stored);
|
|
586
|
+
} catch {
|
|
587
|
+
// Catalog persistence is best-effort; bundled fallbacks remain available.
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function persistMetaModels(
|
|
593
|
+
context: CompatibleRefreshContext,
|
|
594
|
+
entry: ModelsStoreEntry,
|
|
595
|
+
): Promise<void> {
|
|
596
|
+
if (context.publish) {
|
|
597
|
+
await context.publish({ persist: entry });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
await context.store?.write(entry);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function refreshMetaModels(
|
|
604
|
+
context: RefreshModelsContext,
|
|
605
|
+
fetchImpl: Fetch = fetch,
|
|
606
|
+
): Promise<MetaProviderModel[]> {
|
|
607
|
+
// SAFETY: CompatibleRefreshContext is the union of the Pi 0.83 and 0.84
|
|
608
|
+
// refresh-context fields that this adapter probes defensively at runtime.
|
|
609
|
+
const compatibleContext = context as unknown as CompatibleRefreshContext;
|
|
610
|
+
if (!context.allowNetwork || context.signal?.aborted) {
|
|
611
|
+
const cached = await cachedMetaModels(compatibleContext);
|
|
612
|
+
return cached.length > 0 ? cached : [...FALLBACK_MODELS];
|
|
613
|
+
}
|
|
614
|
+
const apiKey =
|
|
615
|
+
context.credential?.type === "oauth"
|
|
616
|
+
? context.credential.access
|
|
617
|
+
: context.credential?.type === "api_key"
|
|
618
|
+
? context.credential.key
|
|
619
|
+
: undefined;
|
|
620
|
+
if (!apiKey) {
|
|
621
|
+
const cached = await cachedMetaModels(compatibleContext);
|
|
622
|
+
return cached.length > 0 ? cached : [...FALLBACK_MODELS];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
const response = await fetchImpl(META_MODEL_CATALOG_URL, {
|
|
627
|
+
headers: {
|
|
628
|
+
Accept: "application/json",
|
|
629
|
+
Authorization: `Bearer ${apiKey}`,
|
|
630
|
+
"x-api-version": "1.0.0",
|
|
631
|
+
},
|
|
632
|
+
signal: context.signal,
|
|
633
|
+
});
|
|
634
|
+
const body = (await responseBody(response)) as CatalogResponse &
|
|
635
|
+
Record<string, unknown>;
|
|
636
|
+
if (!response.ok) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Meta model catalog failed (HTTP ${response.status})${errorDetail(body) ? `: ${errorDetail(body)}` : ""}`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const models = toProviderModels(body);
|
|
642
|
+
if (models.length === 0) {
|
|
643
|
+
const cached = await cachedMetaModels(compatibleContext);
|
|
644
|
+
return cached.length > 0 ? cached : [...FALLBACK_MODELS];
|
|
645
|
+
}
|
|
646
|
+
if (!context.signal?.aborted) {
|
|
647
|
+
try {
|
|
648
|
+
await persistMetaModels(compatibleContext, {
|
|
649
|
+
models: modelsForStore(models),
|
|
650
|
+
checkedAt: Date.now(),
|
|
651
|
+
});
|
|
652
|
+
} catch {
|
|
653
|
+
// Keep the fresh catalog usable even if persistence fails.
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return models;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (context.signal?.aborted) throw error;
|
|
659
|
+
const cached = await cachedMetaModels(compatibleContext);
|
|
660
|
+
return cached.length > 0 ? cached : [...FALLBACK_MODELS];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function metaFallbackCost(
|
|
665
|
+
modelId: string,
|
|
666
|
+
): MetaProviderModel["cost"] | undefined {
|
|
667
|
+
return FALLBACK_MODELS.find((model) => model.id === modelId)?.cost;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Meta prompt-cache opt-in. Measured 0% hits on /chat/completions vs 93–99% on /responses with 24h. */
|
|
671
|
+
export const META_PROMPT_CACHE_RETENTION = "24h";
|
|
672
|
+
|
|
673
|
+
const ENCRYPTED_REASONING_INCLUDE = "reasoning.encrypted_content";
|
|
674
|
+
const PROBE_RETRY_MS = 5 * 60 * 1000;
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Keys minted through /muse-code/key are not always entitled to encrypted
|
|
678
|
+
* reasoning replay (Meta answers HTTP 400 "reasoning `encrypted_content`
|
|
679
|
+
* was not issued to this caller" when they aren't). Entitlement can change
|
|
680
|
+
* between minted keys, so probe once per key per process instead of
|
|
681
|
+
* hard-coding a decision: requests never 400 and reasoning continuity is
|
|
682
|
+
* kept whenever the key allows it.
|
|
683
|
+
*/
|
|
684
|
+
interface EntitlementCache {
|
|
685
|
+
keyHash?: string;
|
|
686
|
+
known?: boolean;
|
|
687
|
+
lastAttemptAt: number;
|
|
688
|
+
}
|
|
689
|
+
const entitlementCache: EntitlementCache = { lastAttemptAt: 0 };
|
|
690
|
+
|
|
691
|
+
function apiKeyHash(key: string): string {
|
|
692
|
+
// Non-cryptographic FNV-1a; only used to key the in-process probe cache.
|
|
693
|
+
let hash = 2166136261;
|
|
694
|
+
for (let i = 0; i < key.length; i++) {
|
|
695
|
+
hash ^= key.charCodeAt(i);
|
|
696
|
+
hash = Math.imul(hash, 16777619);
|
|
697
|
+
}
|
|
698
|
+
return (hash >>> 0).toString(16);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Probe whether the given API key can request reasoning.encrypted_content.
|
|
703
|
+
* Returns true (200), false (Meta rejects the include), or undefined when
|
|
704
|
+
* the probe was inconclusive (transient error) and must be retried later.
|
|
705
|
+
*/
|
|
706
|
+
export async function probeEncryptedReasoningEntitlement(
|
|
707
|
+
apiKey: string,
|
|
708
|
+
fetchImpl: Fetch = fetch,
|
|
709
|
+
): Promise<boolean | undefined> {
|
|
710
|
+
try {
|
|
711
|
+
const response = await fetchImpl(`${META_API_BASE_URL}/responses`, {
|
|
712
|
+
method: "POST",
|
|
713
|
+
headers: {
|
|
714
|
+
Accept: "application/json",
|
|
715
|
+
Authorization: `Bearer ${apiKey}`,
|
|
716
|
+
"Content-Type": "application/json",
|
|
717
|
+
"x-api-version": "1.0.0",
|
|
718
|
+
},
|
|
719
|
+
body: JSON.stringify({
|
|
720
|
+
model: "muse-spark-1.3",
|
|
721
|
+
input: "Answer with the single letter: a",
|
|
722
|
+
include: [ENCRYPTED_REASONING_INCLUDE],
|
|
723
|
+
max_output_tokens: 16,
|
|
724
|
+
store: false,
|
|
725
|
+
}),
|
|
726
|
+
});
|
|
727
|
+
if (response.status === 200) return true;
|
|
728
|
+
if (response.status === 400) {
|
|
729
|
+
const text = await response.text();
|
|
730
|
+
if (text.includes("encrypted_content")) return false;
|
|
731
|
+
}
|
|
732
|
+
return undefined;
|
|
733
|
+
} catch {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function scheduleEntitlementProbe(apiKey: string): void {
|
|
739
|
+
const hash = apiKeyHash(apiKey);
|
|
740
|
+
if (
|
|
741
|
+
entitlementCache.keyHash === hash &&
|
|
742
|
+
entitlementCache.known !== undefined
|
|
743
|
+
) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (Date.now() - entitlementCache.lastAttemptAt < PROBE_RETRY_MS) return;
|
|
747
|
+
entitlementCache.lastAttemptAt = Date.now();
|
|
748
|
+
void probeEncryptedReasoningEntitlement(apiKey).then((known) => {
|
|
749
|
+
if (known !== undefined) {
|
|
750
|
+
entitlementCache.keyHash = hash;
|
|
751
|
+
entitlementCache.known = known;
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function keepEncryptedReasoningFor(apiKey: string | undefined): boolean {
|
|
757
|
+
if (!apiKey) return false;
|
|
758
|
+
if (entitlementCache.keyHash !== apiKeyHash(apiKey)) return false;
|
|
759
|
+
return entitlementCache.known === true;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
763
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
764
|
+
? (value as Record<string, unknown>)
|
|
765
|
+
: undefined;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Hermes-equivalent Responses hints for api.meta.ai:
|
|
770
|
+
* setdefault `prompt_cache_retention: 24h`, and drop `reasoning.effort: none`
|
|
771
|
+
* because Meta 400s on it.
|
|
772
|
+
*/
|
|
773
|
+
export function applyMetaResponsesCacheHints(
|
|
774
|
+
payload: unknown,
|
|
775
|
+
keepEncryptedReasoning = false,
|
|
776
|
+
): Record<string, unknown> | undefined {
|
|
777
|
+
const body = asRecord(payload);
|
|
778
|
+
if (!body) return undefined;
|
|
779
|
+
if (body.prompt_cache_retention === undefined) {
|
|
780
|
+
body.prompt_cache_retention = META_PROMPT_CACHE_RETENTION;
|
|
781
|
+
}
|
|
782
|
+
// Drop the encrypted-reasoning include unless the key was probed and
|
|
783
|
+
// found entitled: unentitled keys get a fatal HTTP 400 for it (see
|
|
784
|
+
// probeEncryptedReasoningEntitlement). Other include entries survive.
|
|
785
|
+
if (!keepEncryptedReasoning && Array.isArray(body.include)) {
|
|
786
|
+
const include = body.include.filter(
|
|
787
|
+
(item) => item !== "reasoning.encrypted_content",
|
|
788
|
+
);
|
|
789
|
+
if (include.length === 0) delete body.include;
|
|
790
|
+
else body.include = include;
|
|
791
|
+
}
|
|
792
|
+
const reasoning = asRecord(body.reasoning);
|
|
793
|
+
if (
|
|
794
|
+
reasoning &&
|
|
795
|
+
(reasoning.effort === "none" ||
|
|
796
|
+
reasoning.effort === undefined ||
|
|
797
|
+
reasoning.effort === null)
|
|
798
|
+
) {
|
|
799
|
+
delete body.reasoning;
|
|
800
|
+
}
|
|
801
|
+
return body;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function createMetaProviderConfig(): ProviderConfig {
|
|
805
|
+
return {
|
|
806
|
+
name: "Meta Model API",
|
|
807
|
+
baseUrl: META_API_BASE_URL,
|
|
808
|
+
api: "openai-responses",
|
|
809
|
+
apiKey: "$META_API_KEY",
|
|
810
|
+
models: [...FALLBACK_MODELS],
|
|
811
|
+
refreshModels: refreshMetaModels,
|
|
812
|
+
oauth: {
|
|
813
|
+
name: "Meta Model API (browser login or API key)",
|
|
814
|
+
login: loginMeta,
|
|
815
|
+
refreshToken: refreshMetaToken,
|
|
816
|
+
getApiKey: (credentials: { access: string }) => credentials.access,
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export default function metaOAuthProvider(pi: ExtensionAPI): void {
|
|
822
|
+
// Allow MODEL_API_KEY as fallback for API-key users — shim to META_API_KEY so $META_API_KEY interpolation works.
|
|
823
|
+
if (
|
|
824
|
+
process.env[META_ENV_VAR] === undefined &&
|
|
825
|
+
process.env["MODEL_API_KEY"] !== undefined
|
|
826
|
+
) {
|
|
827
|
+
process.env[META_ENV_VAR] = process.env["MODEL_API_KEY"];
|
|
828
|
+
}
|
|
829
|
+
if (
|
|
830
|
+
process.env["MODEL_API_KEY"] === undefined &&
|
|
831
|
+
process.env[META_ENV_VAR] !== undefined
|
|
832
|
+
) {
|
|
833
|
+
process.env["MODEL_API_KEY"] = process.env[META_ENV_VAR];
|
|
834
|
+
}
|
|
835
|
+
pi.registerProvider(META_PROVIDER_ID, createMetaProviderConfig());
|
|
836
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
837
|
+
if (ctx.model?.provider !== META_PROVIDER_ID) return undefined;
|
|
838
|
+
let apiKey: string | undefined;
|
|
839
|
+
try {
|
|
840
|
+
apiKey = (await ctx.modelRegistry?.getProviderAuth(META_PROVIDER_ID))
|
|
841
|
+
?.auth?.apiKey;
|
|
842
|
+
} catch {
|
|
843
|
+
apiKey = undefined;
|
|
844
|
+
}
|
|
845
|
+
if (apiKey) scheduleEntitlementProbe(apiKey);
|
|
846
|
+
return applyMetaResponsesCacheHints(
|
|
847
|
+
event.payload,
|
|
848
|
+
keepEncryptedReasoningFor(apiKey),
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geocine/pi-meta-oauth",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Meta Model API OAuth provider for pi",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"meta",
|
|
8
|
+
"muse-spark",
|
|
9
|
+
"oauth",
|
|
10
|
+
"provider"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"files": [
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md",
|
|
20
|
+
"extensions/"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/BlockedPath/pi-meta-oauth"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/BlockedPath/pi-meta-oauth#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/BlockedPath/pi-meta-oauth/issues"
|
|
29
|
+
},
|
|
30
|
+
"pi": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./extensions/meta.ts"
|
|
33
|
+
],
|
|
34
|
+
"image": "https://raw.githubusercontent.com/BlockedPath/pi-meta-oauth/main/preview.png"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-ai": ">=0.83.0 <0.86.0",
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=0.83.0 <0.86.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
43
|
+
"bun-types": "latest",
|
|
44
|
+
"typescript": "^5.9.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "bun test"
|
|
49
|
+
}
|
|
50
|
+
}
|