@earendil-works/pi-ai 0.80.5 → 0.80.7
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 +7 -4
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +102 -50
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.d.ts +1 -1
- package/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +19 -11
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/google-generative-ai.d.ts.map +1 -1
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts +2 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +6 -3
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +1 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +12 -3
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts +3 -1
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +57 -4
- package/dist/api/openai-responses-shared.js.map +1 -1
- package/dist/api/openai-responses.d.ts +2 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +24 -8
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/pi-messages.d.ts +97 -0
- package/dist/api/pi-messages.d.ts.map +1 -0
- package/dist/api/pi-messages.js +309 -0
- package/dist/api/pi-messages.js.map +1 -0
- package/dist/api/pi-messages.lazy.d.ts +3 -0
- package/dist/api/pi-messages.lazy.d.ts.map +1 -0
- package/dist/api/pi-messages.lazy.js +3 -0
- package/dist/api/pi-messages.lazy.js.map +1 -0
- package/dist/api/simple-options.d.ts +1 -1
- package/dist/api/simple-options.d.ts.map +1 -1
- package/dist/api/simple-options.js +1 -1
- package/dist/api/simple-options.js.map +1 -1
- package/dist/compat.d.ts +2 -0
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +5 -1
- package/dist/compat.js.map +1 -1
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +1 -0
- package/dist/env-api-keys.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +706 -215
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.js +15 -6
- package/dist/models.js.map +1 -1
- package/dist/providers/all.d.ts +9 -5
- package/dist/providers/all.d.ts.map +1 -1
- package/dist/providers/all.js.map +1 -1
- package/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.js +5 -1
- package/dist/providers/amazon-bedrock.js.map +1 -1
- package/dist/providers/amazon-bedrock.models.d.ts +122 -5
- package/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.models.js +86 -20
- package/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/dist/providers/anthropic.models.d.ts +11 -1
- package/dist/providers/anthropic.models.d.ts.map +1 -1
- package/dist/providers/anthropic.models.js +6 -4
- package/dist/providers/anthropic.models.js.map +1 -1
- package/dist/providers/azure-openai-responses.models.d.ts +7 -8
- package/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.models.js +20 -21
- package/dist/providers/azure-openai-responses.models.js.map +1 -1
- package/dist/providers/cerebras.models.d.ts.map +1 -1
- package/dist/providers/cerebras.models.js +1 -1
- package/dist/providers/cerebras.models.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.d.ts +103 -1
- package/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.js +78 -4
- package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
- package/dist/providers/cloudflare-auth.d.ts.map +1 -1
- package/dist/providers/cloudflare-auth.js +9 -6
- package/dist/providers/cloudflare-auth.js.map +1 -1
- package/dist/providers/deepseek.models.d.ts +2 -2
- package/dist/providers/deepseek.models.d.ts.map +1 -1
- package/dist/providers/deepseek.models.js +2 -2
- package/dist/providers/deepseek.models.js.map +1 -1
- package/dist/providers/fireworks.models.d.ts +2 -2
- package/dist/providers/fireworks.models.d.ts.map +1 -1
- package/dist/providers/fireworks.models.js +2 -2
- package/dist/providers/fireworks.models.js.map +1 -1
- package/dist/providers/github-copilot.models.d.ts +101 -8
- package/dist/providers/github-copilot.models.d.ts.map +1 -1
- package/dist/providers/github-copilot.models.js +65 -7
- package/dist/providers/github-copilot.models.js.map +1 -1
- package/dist/providers/openai-codex.models.d.ts +56 -0
- package/dist/providers/openai-codex.models.d.ts.map +1 -1
- package/dist/providers/openai-codex.models.js +20 -9
- package/dist/providers/openai-codex.models.js.map +1 -1
- package/dist/providers/openai.models.d.ts +77 -8
- package/dist/providers/openai.models.d.ts.map +1 -1
- package/dist/providers/openai.models.js +37 -24
- package/dist/providers/openai.models.js.map +1 -1
- package/dist/providers/opencode-go.models.d.ts +3 -3
- package/dist/providers/opencode-go.models.d.ts.map +1 -1
- package/dist/providers/opencode-go.models.js +3 -3
- package/dist/providers/opencode-go.models.js.map +1 -1
- package/dist/providers/opencode.models.d.ts +137 -4
- package/dist/providers/opencode.models.d.ts.map +1 -1
- package/dist/providers/opencode.models.js +83 -8
- package/dist/providers/opencode.models.js.map +1 -1
- package/dist/providers/openrouter.models.d.ts +48 -44
- package/dist/providers/openrouter.models.d.ts.map +1 -1
- package/dist/providers/openrouter.models.js +112 -127
- package/dist/providers/openrouter.models.js.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.d.ts +19 -111
- package/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.js +42 -142
- package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/dist/providers/zai-coding-cn.models.d.ts +1 -1
- package/dist/providers/zai-coding-cn.models.d.ts.map +1 -1
- package/dist/providers/zai-coding-cn.models.js +1 -1
- package/dist/providers/zai-coding-cn.models.js.map +1 -1
- package/dist/providers/zai.models.d.ts +1 -1
- package/dist/providers/zai.models.d.ts.map +1 -1
- package/dist/providers/zai.models.js +1 -1
- package/dist/providers/zai.models.js.map +1 -1
- package/dist/types.d.ts +41 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/deferred-tools.d.ts +9 -0
- package/dist/utils/deferred-tools.d.ts.map +1 -0
- package/dist/utils/deferred-tools.js +36 -0
- package/dist/utils/deferred-tools.js.map +1 -0
- package/dist/utils/estimate.d.ts +3 -3
- package/dist/utils/estimate.d.ts.map +1 -1
- package/dist/utils/estimate.js +35 -14
- package/dist/utils/estimate.js.map +1 -1
- package/dist/utils/oauth/index.d.ts +1 -0
- package/dist/utils/oauth/index.d.ts.map +1 -1
- package/dist/utils/oauth/index.js +9 -0
- package/dist/utils/oauth/index.js.map +1 -1
- package/dist/utils/oauth/radius.d.ts +39 -0
- package/dist/utils/oauth/radius.d.ts.map +1 -0
- package/dist/utils/oauth/radius.js +415 -0
- package/dist/utils/oauth/radius.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radius gateway OAuth flow and model catalog loading.
|
|
3
|
+
*
|
|
4
|
+
* Radius is a pi-messages gateway. OAuth endpoints are discovered from the
|
|
5
|
+
* gateway (`/v1/oauth`); the model catalog comes from `/v1/config` and is
|
|
6
|
+
* cached on the stored credential (`gatewayConfig`) so models are available
|
|
7
|
+
* at startup and refreshed whenever the token refreshes.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: This module uses node:http for the OAuth callback server.
|
|
10
|
+
* It is only intended for CLI use, not browser environments.
|
|
11
|
+
*/
|
|
12
|
+
// NEVER convert to top-level imports - breaks browser/Vite builds
|
|
13
|
+
let _http = null;
|
|
14
|
+
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
|
|
15
|
+
import("node:http").then((m) => {
|
|
16
|
+
_http = m;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
20
|
+
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
|
|
21
|
+
import { generatePKCE } from "./pkce.js";
|
|
22
|
+
export const DEFAULT_RADIUS_GATEWAY = "https://radius.pi.dev";
|
|
23
|
+
const CALLBACK_HOST = "127.0.0.1";
|
|
24
|
+
const CALLBACK_PORT = 1456;
|
|
25
|
+
const CALLBACK_PATH = "/oauth/callback";
|
|
26
|
+
const REDIRECT_URI = `http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
27
|
+
const TOKEN_EXPIRY_SKEW_MS = 60_000;
|
|
28
|
+
const LOGIN_METHOD_BROWSER = "browser";
|
|
29
|
+
const LOGIN_METHOD_DEVICE_CODE = "device-code";
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function normalizeRadiusGatewayUrl(value) {
|
|
34
|
+
const withScheme = /^https?:\/\//iu.test(value) ? value : `https://${value}`;
|
|
35
|
+
return withScheme.replace(/\/+$/u, "");
|
|
36
|
+
}
|
|
37
|
+
// The gateway is a trusted first-party service. The shape checks below only
|
|
38
|
+
// guard against version skew and stale credential caches: malformed entries
|
|
39
|
+
// are dropped rather than failing the whole catalog, and nested fields (e.g.
|
|
40
|
+
// `input` members, `cost` rates) are intentionally not validated in depth.
|
|
41
|
+
// Do not turn this into strict validation.
|
|
42
|
+
function isRadiusGatewayModel(value) {
|
|
43
|
+
if (!isRecord(value)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return (typeof value.id === "string" &&
|
|
47
|
+
typeof value.name === "string" &&
|
|
48
|
+
typeof value.reasoning === "boolean" &&
|
|
49
|
+
Array.isArray(value.input) &&
|
|
50
|
+
isRecord(value.cost) &&
|
|
51
|
+
typeof value.contextWindow === "number" &&
|
|
52
|
+
typeof value.maxTokens === "number");
|
|
53
|
+
}
|
|
54
|
+
function sanitizeRadiusGatewayConfig(config) {
|
|
55
|
+
if (!isRecord(config)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const baseUrl = config.baseUrl;
|
|
59
|
+
const models = config.models;
|
|
60
|
+
if (typeof baseUrl !== "string" || !Array.isArray(models)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
baseUrl,
|
|
65
|
+
models: models.filter(isRadiusGatewayModel).map((model) => ({ ...model })),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function getRadiusCredentialConfig(credentials) {
|
|
69
|
+
return sanitizeRadiusGatewayConfig(credentials?.gatewayConfig);
|
|
70
|
+
}
|
|
71
|
+
function truncateHttpBody(body) {
|
|
72
|
+
const trimmed = body.trim();
|
|
73
|
+
return trimmed.length > 512 ? `${trimmed.slice(0, 512)}…` : trimmed;
|
|
74
|
+
}
|
|
75
|
+
async function loadRadiusGatewayConfig(gateway, apiKey) {
|
|
76
|
+
const headers = { accept: "application/json" };
|
|
77
|
+
if (apiKey) {
|
|
78
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(new URL("/v1/config", gateway), { headers });
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`Could not load Radius config from ${gateway}: ${response.status}: ${truncateHttpBody(await response.text())}`);
|
|
83
|
+
}
|
|
84
|
+
const config = sanitizeRadiusGatewayConfig(await response.json());
|
|
85
|
+
if (!config) {
|
|
86
|
+
throw new Error(`Invalid Radius config from ${gateway}`);
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
async function loadRadiusOAuthConfig(gateway) {
|
|
91
|
+
const response = await fetch(new URL("/v1/oauth", gateway), {
|
|
92
|
+
headers: { accept: "application/json" },
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Could not load Radius OAuth config from ${gateway}: ${response.status} ${await response.text()}`);
|
|
96
|
+
}
|
|
97
|
+
return (await response.json());
|
|
98
|
+
}
|
|
99
|
+
class OAuthResponseError extends Error {
|
|
100
|
+
status;
|
|
101
|
+
oauthError;
|
|
102
|
+
constructor(status, oauthError, description, message) {
|
|
103
|
+
const detail = oauthError
|
|
104
|
+
? description
|
|
105
|
+
? `${oauthError}: ${description}`
|
|
106
|
+
: oauthError
|
|
107
|
+
: description || String(status);
|
|
108
|
+
super(`${message}: ${detail}`);
|
|
109
|
+
this.status = status;
|
|
110
|
+
this.oauthError = oauthError;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function readOAuthResponseError(response, message) {
|
|
114
|
+
const text = await response.text().catch(() => "");
|
|
115
|
+
let oauthError;
|
|
116
|
+
let description;
|
|
117
|
+
if (text) {
|
|
118
|
+
try {
|
|
119
|
+
const data = JSON.parse(text);
|
|
120
|
+
oauthError = typeof data.error === "string" ? data.error : undefined;
|
|
121
|
+
description = typeof data.error_description === "string" ? data.error_description : undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
description = text;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return new OAuthResponseError(response.status, oauthError, description, message);
|
|
128
|
+
}
|
|
129
|
+
async function requestOAuthToken(oauth, body, signal) {
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetch(oauth.tokenEndpoint, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
|
|
135
|
+
body,
|
|
136
|
+
signal,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
throw new Error("Login cancelled");
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw await readOAuthResponseError(response, "Radius OAuth token request failed");
|
|
147
|
+
}
|
|
148
|
+
const data = (await response.json());
|
|
149
|
+
return {
|
|
150
|
+
access: data.access_token,
|
|
151
|
+
refresh: data.refresh_token,
|
|
152
|
+
expires: Date.now() + data.expires_in * 1000 - TOKEN_EXPIRY_SKEW_MS,
|
|
153
|
+
scope: data.scope,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function startOAuthCallbackServer(expectedState, signal) {
|
|
157
|
+
if (!_http) {
|
|
158
|
+
throw new Error("Radius OAuth is only available in Node.js environments");
|
|
159
|
+
}
|
|
160
|
+
let settle = () => { };
|
|
161
|
+
let settled = false;
|
|
162
|
+
const wait = new Promise((resolve) => {
|
|
163
|
+
settle = resolve;
|
|
164
|
+
});
|
|
165
|
+
const finish = (code) => {
|
|
166
|
+
if (settled) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
settled = true;
|
|
170
|
+
signal?.removeEventListener("abort", onAbort);
|
|
171
|
+
settle(code);
|
|
172
|
+
};
|
|
173
|
+
const onAbort = () => finish(null);
|
|
174
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
175
|
+
const sendPage = (response, status, html) => {
|
|
176
|
+
response.statusCode = status;
|
|
177
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
178
|
+
response.end(html);
|
|
179
|
+
};
|
|
180
|
+
const server = _http.createServer((request, response) => {
|
|
181
|
+
const url = new URL(request.url ?? "/", REDIRECT_URI);
|
|
182
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
183
|
+
sendPage(response, 404, oauthErrorHtml("Callback route not found."));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
187
|
+
sendPage(response, 400, oauthErrorHtml("OAuth state mismatch."));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const error = url.searchParams.get("error");
|
|
191
|
+
if (error) {
|
|
192
|
+
sendPage(response, 400, oauthErrorHtml(url.searchParams.get("error_description") ?? error));
|
|
193
|
+
finish(null);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const code = url.searchParams.get("code");
|
|
197
|
+
if (!code) {
|
|
198
|
+
sendPage(response, 400, oauthErrorHtml("Missing authorization code."));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
sendPage(response, 200, oauthSuccessHtml("Signed in to Radius. You may now close this page."));
|
|
202
|
+
finish(code);
|
|
203
|
+
});
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
server
|
|
206
|
+
.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
207
|
+
resolve({
|
|
208
|
+
waitForCode: () => wait,
|
|
209
|
+
close: () => {
|
|
210
|
+
finish(null);
|
|
211
|
+
server.close();
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
})
|
|
215
|
+
.once("error", () => {
|
|
216
|
+
finish(null);
|
|
217
|
+
resolve({ waitForCode: async () => null, close: () => { } });
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async function loginWithBrowser(oauth, callbacks) {
|
|
222
|
+
const { verifier, challenge } = await generatePKCE();
|
|
223
|
+
const state = crypto.randomUUID();
|
|
224
|
+
const authorizeUrl = new URL(oauth.authorizationEndpoint);
|
|
225
|
+
authorizeUrl.search = new URLSearchParams({
|
|
226
|
+
response_type: "code",
|
|
227
|
+
client_id: oauth.clientId,
|
|
228
|
+
redirect_uri: REDIRECT_URI,
|
|
229
|
+
scope: oauth.scope,
|
|
230
|
+
code_challenge: challenge,
|
|
231
|
+
code_challenge_method: "S256",
|
|
232
|
+
handoff: "url",
|
|
233
|
+
state,
|
|
234
|
+
}).toString();
|
|
235
|
+
const callbackServer = await startOAuthCallbackServer(state, callbacks.signal);
|
|
236
|
+
callbacks.onProgress?.(`Listening for OAuth callback on ${REDIRECT_URI}`);
|
|
237
|
+
callbacks.onAuth({
|
|
238
|
+
url: authorizeUrl.toString(),
|
|
239
|
+
instructions: "Continue in your browser.",
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
const code = await callbackServer.waitForCode();
|
|
243
|
+
if (!code) {
|
|
244
|
+
if (callbacks.signal?.aborted) {
|
|
245
|
+
throw new Error("Login cancelled");
|
|
246
|
+
}
|
|
247
|
+
throw new Error("OAuth callback did not complete.");
|
|
248
|
+
}
|
|
249
|
+
return await requestOAuthToken(oauth, new URLSearchParams({
|
|
250
|
+
grant_type: "authorization_code",
|
|
251
|
+
client_id: oauth.clientId,
|
|
252
|
+
redirect_uri: REDIRECT_URI,
|
|
253
|
+
code,
|
|
254
|
+
code_verifier: verifier,
|
|
255
|
+
}), callbacks.signal);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
callbackServer.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function requestDeviceAuthorization(oauth, signal) {
|
|
262
|
+
let response;
|
|
263
|
+
try {
|
|
264
|
+
response = await fetch(oauth.deviceAuthorizationEndpoint, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
|
|
267
|
+
body: new URLSearchParams({ client_id: oauth.clientId, scope: oauth.scope }),
|
|
268
|
+
signal,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (signal?.aborted) {
|
|
273
|
+
throw new Error("Login cancelled");
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
throw await readOAuthResponseError(response, "Radius OAuth device authorization failed");
|
|
279
|
+
}
|
|
280
|
+
const data = (await response.json());
|
|
281
|
+
if (!data.device_code || !data.user_code || !data.expires_in) {
|
|
282
|
+
throw new Error("Radius OAuth device authorization response is missing required fields");
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
device_code: data.device_code,
|
|
286
|
+
user_code: data.user_code,
|
|
287
|
+
verification_uri: data.verification_uri,
|
|
288
|
+
verification_uri_complete: data.verification_uri_complete,
|
|
289
|
+
expires_in: data.expires_in,
|
|
290
|
+
interval: data.interval,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function loginWithDeviceCode(oauth, callbacks) {
|
|
294
|
+
const device = await requestDeviceAuthorization(oauth, callbacks.signal);
|
|
295
|
+
callbacks.onDeviceCode({
|
|
296
|
+
userCode: device.user_code,
|
|
297
|
+
verificationUri: device.verification_uri || oauth.verificationEndpoint,
|
|
298
|
+
intervalSeconds: device.interval,
|
|
299
|
+
expiresInSeconds: device.expires_in,
|
|
300
|
+
});
|
|
301
|
+
return pollOAuthDeviceCodeFlow({
|
|
302
|
+
intervalSeconds: device.interval,
|
|
303
|
+
expiresInSeconds: device.expires_in,
|
|
304
|
+
signal: callbacks.signal,
|
|
305
|
+
poll: async () => {
|
|
306
|
+
try {
|
|
307
|
+
const credentials = await requestOAuthToken(oauth, new URLSearchParams({
|
|
308
|
+
grant_type: oauth.deviceCodeGrantType,
|
|
309
|
+
client_id: oauth.clientId,
|
|
310
|
+
device_code: device.device_code,
|
|
311
|
+
}), callbacks.signal);
|
|
312
|
+
return { status: "complete", value: credentials };
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (!(error instanceof OAuthResponseError)) {
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
switch (error.oauthError) {
|
|
319
|
+
case "authorization_pending":
|
|
320
|
+
return { status: "pending" };
|
|
321
|
+
case "slow_down":
|
|
322
|
+
return { status: "slow_down" };
|
|
323
|
+
case "expired_token":
|
|
324
|
+
return { status: "failed", message: "Device authorization expired." };
|
|
325
|
+
case "access_denied":
|
|
326
|
+
return { status: "failed", message: "Device authorization was denied." };
|
|
327
|
+
default:
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
async function attachGatewayConfig(gateway, credentials, previous) {
|
|
335
|
+
try {
|
|
336
|
+
const config = await loadRadiusGatewayConfig(gateway, credentials.access);
|
|
337
|
+
return { ...credentials, gatewayConfig: config };
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
// Keep the previous catalog so models do not vanish on transient
|
|
341
|
+
// config failures; the next token refresh retries.
|
|
342
|
+
const previousConfig = getRadiusCredentialConfig(previous);
|
|
343
|
+
if (previousConfig) {
|
|
344
|
+
return { ...credentials, gatewayConfig: previousConfig };
|
|
345
|
+
}
|
|
346
|
+
// No catalog to retain (e.g. initial login): fail loudly instead of
|
|
347
|
+
// completing a sign-in that would register no models.
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
export function createRadiusOAuthProvider(options) {
|
|
352
|
+
const gateway = normalizeRadiusGatewayUrl(options.gateway);
|
|
353
|
+
return {
|
|
354
|
+
id: options.id,
|
|
355
|
+
name: options.name,
|
|
356
|
+
async login(callbacks) {
|
|
357
|
+
const oauth = await loadRadiusOAuthConfig(gateway);
|
|
358
|
+
const loginMethod = await callbacks.onSelect({
|
|
359
|
+
message: `Sign in to ${options.name}:`,
|
|
360
|
+
options: [
|
|
361
|
+
{ id: LOGIN_METHOD_BROWSER, label: "Sign in with browser (recommended)" },
|
|
362
|
+
{
|
|
363
|
+
id: LOGIN_METHOD_DEVICE_CODE,
|
|
364
|
+
label: "Sign in with device code (when signing in from another device)",
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
if (!loginMethod) {
|
|
369
|
+
throw new Error("Login cancelled");
|
|
370
|
+
}
|
|
371
|
+
let credentials;
|
|
372
|
+
if (loginMethod === LOGIN_METHOD_DEVICE_CODE) {
|
|
373
|
+
credentials = await loginWithDeviceCode(oauth, callbacks);
|
|
374
|
+
}
|
|
375
|
+
else if (loginMethod === LOGIN_METHOD_BROWSER) {
|
|
376
|
+
credentials = await loginWithBrowser(oauth, callbacks);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
throw new Error(`Unknown ${options.name} sign-in method: ${loginMethod}`);
|
|
380
|
+
}
|
|
381
|
+
return attachGatewayConfig(gateway, credentials);
|
|
382
|
+
},
|
|
383
|
+
async refreshToken(credentials) {
|
|
384
|
+
const oauth = await loadRadiusOAuthConfig(gateway);
|
|
385
|
+
const refreshed = await requestOAuthToken(oauth, new URLSearchParams({
|
|
386
|
+
grant_type: "refresh_token",
|
|
387
|
+
client_id: oauth.clientId,
|
|
388
|
+
refresh_token: credentials.refresh,
|
|
389
|
+
}));
|
|
390
|
+
return attachGatewayConfig(gateway, refreshed, credentials);
|
|
391
|
+
},
|
|
392
|
+
getApiKey(credentials) {
|
|
393
|
+
return credentials.access;
|
|
394
|
+
},
|
|
395
|
+
modifyModels(models, credentials) {
|
|
396
|
+
const config = getRadiusCredentialConfig(credentials);
|
|
397
|
+
if (!config) {
|
|
398
|
+
return models;
|
|
399
|
+
}
|
|
400
|
+
// Keep models already registered for this provider (e.g. models.json
|
|
401
|
+
// custom entries) and add catalog models that are not present.
|
|
402
|
+
const existingIds = new Set(models.filter((model) => model.provider === options.id).map((model) => model.id));
|
|
403
|
+
const added = config.models
|
|
404
|
+
.filter((model) => !existingIds.has(model.id))
|
|
405
|
+
.map((model) => ({
|
|
406
|
+
...model,
|
|
407
|
+
api: "pi-messages",
|
|
408
|
+
provider: options.id,
|
|
409
|
+
baseUrl: config.baseUrl,
|
|
410
|
+
}));
|
|
411
|
+
return [...models, ...added];
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=radius.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"radius.js","sourceRoot":"","sources":["../../../src/utils/oauth/radius.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Radius gateway OAuth flow and model catalog loading.\n *\n * Radius is a pi-messages gateway. OAuth endpoints are discovered from the\n * gateway (`/v1/oauth`); the model catalog comes from `/v1/config` and is\n * cached on the stored credential (`gatewayConfig`) so models are available\n * at startup and refreshed whenever the token refreshes.\n *\n * NOTE: This module uses node:http for the OAuth callback server.\n * It is only intended for CLI use, not browser environments.\n */\n\n// NEVER convert to top-level imports - breaks browser/Vite builds\nlet _http: typeof import(\"node:http\") | null = null;\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\timport(\"node:http\").then((m) => {\n\t\t_http = m;\n\t});\n}\n\nimport type { Api, Model, ThinkingLevelMap } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const DEFAULT_RADIUS_GATEWAY = \"https://radius.pi.dev\";\n\nconst CALLBACK_HOST = \"127.0.0.1\";\nconst CALLBACK_PORT = 1456;\nconst CALLBACK_PATH = \"/oauth/callback\";\nconst REDIRECT_URI = `http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;\nconst TOKEN_EXPIRY_SKEW_MS = 60_000;\nconst LOGIN_METHOD_BROWSER = \"browser\";\nconst LOGIN_METHOD_DEVICE_CODE = \"device-code\";\n\n/** Model metadata served by the gateway config endpoint. */\nexport type RadiusGatewayModel = {\n\tid: string;\n\tname: string;\n\treasoning: boolean;\n\tthinkingLevelMap?: ThinkingLevelMap;\n\tinput: (\"text\" | \"image\")[];\n\tcost: Model<Api>[\"cost\"];\n\tcontextWindow: number;\n\tmaxTokens: number;\n};\n\nexport type RadiusGatewayConfig = {\n\tbaseUrl: string;\n\tmodels: RadiusGatewayModel[];\n};\n\nexport type RadiusOAuthCredentials = OAuthCredentials & {\n\tgatewayConfig?: RadiusGatewayConfig;\n};\n\ntype RadiusOAuthConfig = {\n\tissuer: string;\n\tauthorizationEndpoint: string;\n\ttokenEndpoint: string;\n\tdeviceAuthorizationEndpoint: string;\n\tdeviceAuthorizationEventsEndpoint: string;\n\tverificationEndpoint: string;\n\tclientId: string;\n\tscope: string;\n\tdeviceCodeGrantType: string;\n};\n\ntype DeviceAuthorizationResponse = {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in: number;\n\tinterval?: number;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction normalizeRadiusGatewayUrl(value: string): string {\n\tconst withScheme = /^https?:\\/\\//iu.test(value) ? value : `https://${value}`;\n\treturn withScheme.replace(/\\/+$/u, \"\");\n}\n\n// The gateway is a trusted first-party service. The shape checks below only\n// guard against version skew and stale credential caches: malformed entries\n// are dropped rather than failing the whole catalog, and nested fields (e.g.\n// `input` members, `cost` rates) are intentionally not validated in depth.\n// Do not turn this into strict validation.\nfunction isRadiusGatewayModel(value: unknown): value is RadiusGatewayModel {\n\tif (!isRecord(value)) {\n\t\treturn false;\n\t}\n\treturn (\n\t\ttypeof value.id === \"string\" &&\n\t\ttypeof value.name === \"string\" &&\n\t\ttypeof value.reasoning === \"boolean\" &&\n\t\tArray.isArray(value.input) &&\n\t\tisRecord(value.cost) &&\n\t\ttypeof value.contextWindow === \"number\" &&\n\t\ttypeof value.maxTokens === \"number\"\n\t);\n}\n\nfunction sanitizeRadiusGatewayConfig(config: unknown): RadiusGatewayConfig | undefined {\n\tif (!isRecord(config)) {\n\t\treturn undefined;\n\t}\n\tconst baseUrl = config.baseUrl;\n\tconst models = config.models;\n\tif (typeof baseUrl !== \"string\" || !Array.isArray(models)) {\n\t\treturn undefined;\n\t}\n\n\treturn {\n\t\tbaseUrl,\n\t\tmodels: models.filter(isRadiusGatewayModel).map((model) => ({ ...model })),\n\t};\n}\n\nfunction getRadiusCredentialConfig(credentials: OAuthCredentials | undefined): RadiusGatewayConfig | undefined {\n\treturn sanitizeRadiusGatewayConfig((credentials as RadiusOAuthCredentials | undefined)?.gatewayConfig);\n}\n\nfunction truncateHttpBody(body: string): string {\n\tconst trimmed = body.trim();\n\treturn trimmed.length > 512 ? `${trimmed.slice(0, 512)}…` : trimmed;\n}\n\nasync function loadRadiusGatewayConfig(gateway: string, apiKey?: string): Promise<RadiusGatewayConfig> {\n\tconst headers: Record<string, string> = { accept: \"application/json\" };\n\tif (apiKey) {\n\t\theaders.authorization = `Bearer ${apiKey}`;\n\t}\n\n\tconst response = await fetch(new URL(\"/v1/config\", gateway), { headers });\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Could not load Radius config from ${gateway}: ${response.status}: ${truncateHttpBody(await response.text())}`,\n\t\t);\n\t}\n\n\tconst config = sanitizeRadiusGatewayConfig(await response.json());\n\tif (!config) {\n\t\tthrow new Error(`Invalid Radius config from ${gateway}`);\n\t}\n\treturn config;\n}\n\nasync function loadRadiusOAuthConfig(gateway: string): Promise<RadiusOAuthConfig> {\n\tconst response = await fetch(new URL(\"/v1/oauth\", gateway), {\n\t\theaders: { accept: \"application/json\" },\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`Could not load Radius OAuth config from ${gateway}: ${response.status} ${await response.text()}`,\n\t\t);\n\t}\n\n\treturn (await response.json()) as RadiusOAuthConfig;\n}\n\nclass OAuthResponseError extends Error {\n\treadonly status: number;\n\treadonly oauthError?: string;\n\n\tconstructor(status: number, oauthError: string | undefined, description: string | undefined, message: string) {\n\t\tconst detail = oauthError\n\t\t\t? description\n\t\t\t\t? `${oauthError}: ${description}`\n\t\t\t\t: oauthError\n\t\t\t: description || String(status);\n\t\tsuper(`${message}: ${detail}`);\n\t\tthis.status = status;\n\t\tthis.oauthError = oauthError;\n\t}\n}\n\nasync function readOAuthResponseError(response: Response, message: string): Promise<OAuthResponseError> {\n\tconst text = await response.text().catch(() => \"\");\n\tlet oauthError: string | undefined;\n\tlet description: string | undefined;\n\n\tif (text) {\n\t\ttry {\n\t\t\tconst data = JSON.parse(text) as { error?: unknown; error_description?: unknown };\n\t\t\toauthError = typeof data.error === \"string\" ? data.error : undefined;\n\t\t\tdescription = typeof data.error_description === \"string\" ? data.error_description : undefined;\n\t\t} catch {\n\t\t\tdescription = text;\n\t\t}\n\t}\n\n\treturn new OAuthResponseError(response.status, oauthError, description, message);\n}\n\nasync function requestOAuthToken(\n\toauth: RadiusOAuthConfig,\n\tbody: URLSearchParams,\n\tsignal?: AbortSignal,\n): Promise<OAuthCredentials> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(oauth.tokenEndpoint, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { accept: \"application/json\", \"content-type\": \"application/x-www-form-urlencoded\" },\n\t\t\tbody,\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\t\tthrow error;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow await readOAuthResponseError(response, \"Radius OAuth token request failed\");\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t\tscope?: string;\n\t};\n\n\treturn {\n\t\taccess: data.access_token,\n\t\trefresh: data.refresh_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - TOKEN_EXPIRY_SKEW_MS,\n\t\tscope: data.scope,\n\t};\n}\n\ntype OAuthCallbackServer = {\n\twaitForCode(): Promise<string | null>;\n\tclose(): void;\n};\n\nfunction startOAuthCallbackServer(\n\texpectedState: string,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCallbackServer> {\n\tif (!_http) {\n\t\tthrow new Error(\"Radius OAuth is only available in Node.js environments\");\n\t}\n\n\tlet settle: (code: string | null) => void = () => {};\n\tlet settled = false;\n\tconst wait = new Promise<string | null>((resolve) => {\n\t\tsettle = resolve;\n\t});\n\tconst finish = (code: string | null) => {\n\t\tif (settled) {\n\t\t\treturn;\n\t\t}\n\t\tsettled = true;\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\tsettle(code);\n\t};\n\tconst onAbort = () => finish(null);\n\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\tconst sendPage = (response: import(\"node:http\").ServerResponse, status: number, html: string) => {\n\t\tresponse.statusCode = status;\n\t\tresponse.setHeader(\"content-type\", \"text/html; charset=utf-8\");\n\t\tresponse.end(html);\n\t};\n\n\tconst server = _http.createServer((request, response) => {\n\t\tconst url = new URL(request.url ?? \"/\", REDIRECT_URI);\n\t\tif (url.pathname !== CALLBACK_PATH) {\n\t\t\tsendPage(response, 404, oauthErrorHtml(\"Callback route not found.\"));\n\t\t\treturn;\n\t\t}\n\t\tif (url.searchParams.get(\"state\") !== expectedState) {\n\t\t\tsendPage(response, 400, oauthErrorHtml(\"OAuth state mismatch.\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst error = url.searchParams.get(\"error\");\n\t\tif (error) {\n\t\t\tsendPage(response, 400, oauthErrorHtml(url.searchParams.get(\"error_description\") ?? error));\n\t\t\tfinish(null);\n\t\t\treturn;\n\t\t}\n\n\t\tconst code = url.searchParams.get(\"code\");\n\t\tif (!code) {\n\t\t\tsendPage(response, 400, oauthErrorHtml(\"Missing authorization code.\"));\n\t\t\treturn;\n\t\t}\n\n\t\tsendPage(response, 200, oauthSuccessHtml(\"Signed in to Radius. You may now close this page.\"));\n\t\tfinish(code);\n\t});\n\n\treturn new Promise((resolve) => {\n\t\tserver\n\t\t\t.listen(CALLBACK_PORT, CALLBACK_HOST, () => {\n\t\t\t\tresolve({\n\t\t\t\t\twaitForCode: () => wait,\n\t\t\t\t\tclose: () => {\n\t\t\t\t\t\tfinish(null);\n\t\t\t\t\t\tserver.close();\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t})\n\t\t\t.once(\"error\", () => {\n\t\t\t\tfinish(null);\n\t\t\t\tresolve({ waitForCode: async () => null, close: () => {} });\n\t\t\t});\n\t});\n}\n\nasync function loginWithBrowser(oauth: RadiusOAuthConfig, callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst state = crypto.randomUUID();\n\tconst authorizeUrl = new URL(oauth.authorizationEndpoint);\n\tauthorizeUrl.search = new URLSearchParams({\n\t\tresponse_type: \"code\",\n\t\tclient_id: oauth.clientId,\n\t\tredirect_uri: REDIRECT_URI,\n\t\tscope: oauth.scope,\n\t\tcode_challenge: challenge,\n\t\tcode_challenge_method: \"S256\",\n\t\thandoff: \"url\",\n\t\tstate,\n\t}).toString();\n\n\tconst callbackServer = await startOAuthCallbackServer(state, callbacks.signal);\n\tcallbacks.onProgress?.(`Listening for OAuth callback on ${REDIRECT_URI}`);\n\tcallbacks.onAuth({\n\t\turl: authorizeUrl.toString(),\n\t\tinstructions: \"Continue in your browser.\",\n\t});\n\n\ttry {\n\t\tconst code = await callbackServer.waitForCode();\n\t\tif (!code) {\n\t\t\tif (callbacks.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Login cancelled\");\n\t\t\t}\n\t\t\tthrow new Error(\"OAuth callback did not complete.\");\n\t\t}\n\t\treturn await requestOAuthToken(\n\t\t\toauth,\n\t\t\tnew URLSearchParams({\n\t\t\t\tgrant_type: \"authorization_code\",\n\t\t\t\tclient_id: oauth.clientId,\n\t\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\t\tcode,\n\t\t\t\tcode_verifier: verifier,\n\t\t\t}),\n\t\t\tcallbacks.signal,\n\t\t);\n\t} finally {\n\t\tcallbackServer.close();\n\t}\n}\n\nasync function requestDeviceAuthorization(\n\toauth: RadiusOAuthConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<DeviceAuthorizationResponse> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(oauth.deviceAuthorizationEndpoint, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { accept: \"application/json\", \"content-type\": \"application/x-www-form-urlencoded\" },\n\t\t\tbody: new URLSearchParams({ client_id: oauth.clientId, scope: oauth.scope }),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\t\tthrow error;\n\t}\n\n\tif (!response.ok) {\n\t\tthrow await readOAuthResponseError(response, \"Radius OAuth device authorization failed\");\n\t}\n\n\tconst data = (await response.json()) as Partial<DeviceAuthorizationResponse>;\n\tif (!data.device_code || !data.user_code || !data.expires_in) {\n\t\tthrow new Error(\"Radius OAuth device authorization response is missing required fields\");\n\t}\n\n\treturn {\n\t\tdevice_code: data.device_code,\n\t\tuser_code: data.user_code,\n\t\tverification_uri: data.verification_uri,\n\t\tverification_uri_complete: data.verification_uri_complete,\n\t\texpires_in: data.expires_in,\n\t\tinterval: data.interval,\n\t};\n}\n\nasync function loginWithDeviceCode(\n\toauth: RadiusOAuthConfig,\n\tcallbacks: OAuthLoginCallbacks,\n): Promise<OAuthCredentials> {\n\tconst device = await requestDeviceAuthorization(oauth, callbacks.signal);\n\tcallbacks.onDeviceCode({\n\t\tuserCode: device.user_code,\n\t\tverificationUri: device.verification_uri || oauth.verificationEndpoint,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t});\n\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t\tsignal: callbacks.signal,\n\t\tpoll: async () => {\n\t\t\ttry {\n\t\t\t\tconst credentials = await requestOAuthToken(\n\t\t\t\t\toauth,\n\t\t\t\t\tnew URLSearchParams({\n\t\t\t\t\t\tgrant_type: oauth.deviceCodeGrantType,\n\t\t\t\t\t\tclient_id: oauth.clientId,\n\t\t\t\t\t\tdevice_code: device.device_code,\n\t\t\t\t\t}),\n\t\t\t\t\tcallbacks.signal,\n\t\t\t\t);\n\t\t\t\treturn { status: \"complete\", value: credentials };\n\t\t\t} catch (error) {\n\t\t\t\tif (!(error instanceof OAuthResponseError)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tswitch (error.oauthError) {\n\t\t\t\t\tcase \"authorization_pending\":\n\t\t\t\t\t\treturn { status: \"pending\" };\n\t\t\t\t\tcase \"slow_down\":\n\t\t\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t\t\tcase \"expired_token\":\n\t\t\t\t\t\treturn { status: \"failed\", message: \"Device authorization expired.\" };\n\t\t\t\t\tcase \"access_denied\":\n\t\t\t\t\t\treturn { status: \"failed\", message: \"Device authorization was denied.\" };\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t});\n}\n\nasync function attachGatewayConfig(\n\tgateway: string,\n\tcredentials: OAuthCredentials,\n\tprevious?: OAuthCredentials,\n): Promise<RadiusOAuthCredentials> {\n\ttry {\n\t\tconst config = await loadRadiusGatewayConfig(gateway, credentials.access);\n\t\treturn { ...credentials, gatewayConfig: config };\n\t} catch (error) {\n\t\t// Keep the previous catalog so models do not vanish on transient\n\t\t// config failures; the next token refresh retries.\n\t\tconst previousConfig = getRadiusCredentialConfig(previous);\n\t\tif (previousConfig) {\n\t\t\treturn { ...credentials, gatewayConfig: previousConfig };\n\t\t}\n\t\t// No catalog to retain (e.g. initial login): fail loudly instead of\n\t\t// completing a sign-in that would register no models.\n\t\tthrow error;\n\t}\n}\n\nexport interface RadiusOAuthProviderOptions {\n\tid: string;\n\tname: string;\n\tgateway: string;\n}\n\nexport function createRadiusOAuthProvider(options: RadiusOAuthProviderOptions): OAuthProviderInterface {\n\tconst gateway = normalizeRadiusGatewayUrl(options.gateway);\n\n\treturn {\n\t\tid: options.id,\n\t\tname: options.name,\n\n\t\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\t\tconst oauth = await loadRadiusOAuthConfig(gateway);\n\t\t\tconst loginMethod = await callbacks.onSelect({\n\t\t\t\tmessage: `Sign in to ${options.name}:`,\n\t\t\t\toptions: [\n\t\t\t\t\t{ id: LOGIN_METHOD_BROWSER, label: \"Sign in with browser (recommended)\" },\n\t\t\t\t\t{\n\t\t\t\t\t\tid: LOGIN_METHOD_DEVICE_CODE,\n\t\t\t\t\t\tlabel: \"Sign in with device code (when signing in from another device)\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tif (!loginMethod) {\n\t\t\t\tthrow new Error(\"Login cancelled\");\n\t\t\t}\n\n\t\t\tlet credentials: OAuthCredentials;\n\t\t\tif (loginMethod === LOGIN_METHOD_DEVICE_CODE) {\n\t\t\t\tcredentials = await loginWithDeviceCode(oauth, callbacks);\n\t\t\t} else if (loginMethod === LOGIN_METHOD_BROWSER) {\n\t\t\t\tcredentials = await loginWithBrowser(oauth, callbacks);\n\t\t\t} else {\n\t\t\t\tthrow new Error(`Unknown ${options.name} sign-in method: ${loginMethod}`);\n\t\t\t}\n\n\t\t\treturn attachGatewayConfig(gateway, credentials);\n\t\t},\n\n\t\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\t\tconst oauth = await loadRadiusOAuthConfig(gateway);\n\t\t\tconst refreshed = await requestOAuthToken(\n\t\t\t\toauth,\n\t\t\t\tnew URLSearchParams({\n\t\t\t\t\tgrant_type: \"refresh_token\",\n\t\t\t\t\tclient_id: oauth.clientId,\n\t\t\t\t\trefresh_token: credentials.refresh,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn attachGatewayConfig(gateway, refreshed, credentials);\n\t\t},\n\n\t\tgetApiKey(credentials: OAuthCredentials): string {\n\t\t\treturn credentials.access;\n\t\t},\n\n\t\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\t\tconst config = getRadiusCredentialConfig(credentials);\n\t\t\tif (!config) {\n\t\t\t\treturn models;\n\t\t\t}\n\n\t\t\t// Keep models already registered for this provider (e.g. models.json\n\t\t\t// custom entries) and add catalog models that are not present.\n\t\t\tconst existingIds = new Set(models.filter((model) => model.provider === options.id).map((model) => model.id));\n\t\t\tconst added = config.models\n\t\t\t\t.filter((model) => !existingIds.has(model.id))\n\t\t\t\t.map(\n\t\t\t\t\t(model) =>\n\t\t\t\t\t\t({\n\t\t\t\t\t\t\t...model,\n\t\t\t\t\t\t\tapi: \"pi-messages\",\n\t\t\t\t\t\t\tprovider: options.id,\n\t\t\t\t\t\t\tbaseUrl: config.baseUrl,\n\t\t\t\t\t\t}) as Model<Api>,\n\t\t\t\t);\n\n\t\t\treturn [...models, ...added];\n\t\t},\n\t};\n}\n"]}
|