@oh-my-pi/pi-coding-agent 16.1.2 → 16.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -1
- package/dist/cli.js +3046 -3047
- package/dist/types/config/model-resolver.d.ts +3 -3
- package/dist/types/mnemopi/embed-client.d.ts +70 -0
- package/dist/types/mnemopi/embed-protocol.d.ts +52 -0
- package/dist/types/mnemopi/embed-worker.d.ts +12 -0
- package/dist/types/mnemopi/state.d.ts +9 -1
- package/dist/types/session/agent-storage.d.ts +2 -0
- package/dist/types/session/auth-broker-config.d.ts +3 -2
- package/dist/types/session/history-storage.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/utils/image-loading.d.ts +1 -1
- package/dist/types/utils/ipc.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity-auth.d.ts +37 -0
- package/package.json +12 -12
- package/src/cli.ts +8 -0
- package/src/commands/token.ts +52 -33
- package/src/config/append-only-context-mode.ts +45 -0
- package/src/config/model-discovery.ts +3 -0
- package/src/config/model-registry.ts +21 -3
- package/src/config/model-resolver.ts +31 -8
- package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
- package/src/lsp/client.ts +24 -0
- package/src/mnemopi/backend.ts +49 -3
- package/src/mnemopi/embed-client.ts +401 -0
- package/src/mnemopi/embed-protocol.ts +35 -0
- package/src/mnemopi/embed-worker.ts +113 -0
- package/src/mnemopi/state.ts +29 -1
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/theme/theme.ts +69 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +8 -0
- package/src/session/agent-storage.ts +14 -0
- package/src/session/auth-broker-config.ts +2 -1
- package/src/session/history-storage.ts +13 -1
- package/src/stt/asr-client.ts +2 -7
- package/src/tiny/title-client.ts +2 -7
- package/src/tools/image-gen.ts +4 -8
- package/src/tools/render-utils.ts +4 -1
- package/src/tts/tts-client.ts +2 -7
- package/src/utils/image-loading.ts +12 -2
- package/src/utils/ipc.ts +38 -0
- package/src/web/search/providers/perplexity-auth.ts +133 -0
- package/src/web/search/providers/perplexity.ts +2 -125
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { AuthStorage, OAuthAccess } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
export const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
|
|
5
|
+
export const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
|
|
6
|
+
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
7
|
+
export const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export interface ApiConfig {
|
|
10
|
+
type: "api_key";
|
|
11
|
+
apiKey: string;
|
|
12
|
+
provider: "perplexity" | "openrouter";
|
|
13
|
+
chatBaseUrl: string;
|
|
14
|
+
responsesBaseUrl: string;
|
|
15
|
+
modelPrefix: string;
|
|
16
|
+
useResponses: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PerplexityAuth =
|
|
20
|
+
| ApiConfig
|
|
21
|
+
| {
|
|
22
|
+
type: "oauth";
|
|
23
|
+
access: OAuthAccess;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: "cookies";
|
|
27
|
+
cookies: string;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: "anonymous";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface PerplexityAuthOptions {
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
forceRefresh?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
|
|
39
|
+
export async function getApiConfigs(
|
|
40
|
+
authStorage: AuthStorage,
|
|
41
|
+
sessionId: string | undefined,
|
|
42
|
+
options?: PerplexityAuthOptions,
|
|
43
|
+
): Promise<ApiConfig[]> {
|
|
44
|
+
const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
|
|
45
|
+
const configs: ApiConfig[] = [];
|
|
46
|
+
|
|
47
|
+
const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, options);
|
|
48
|
+
if (perplexityKey) {
|
|
49
|
+
configs.push({
|
|
50
|
+
type: "api_key",
|
|
51
|
+
apiKey: perplexityKey,
|
|
52
|
+
provider: "perplexity",
|
|
53
|
+
chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
|
|
54
|
+
responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
|
|
55
|
+
modelPrefix: "",
|
|
56
|
+
useResponses,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, options);
|
|
61
|
+
if (openrouterKey) {
|
|
62
|
+
configs.push({
|
|
63
|
+
type: "api_key",
|
|
64
|
+
apiKey: openrouterKey,
|
|
65
|
+
provider: "openrouter",
|
|
66
|
+
chatBaseUrl: OPENROUTER_BASE_URL,
|
|
67
|
+
responsesBaseUrl: OPENROUTER_BASE_URL,
|
|
68
|
+
modelPrefix: "perplexity/",
|
|
69
|
+
useResponses,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return configs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Decode a Perplexity JWT's `exp` claim, in ms. Returns `undefined` when the
|
|
78
|
+
* token has no `exp` (which is the common case — Perplexity sessions are
|
|
79
|
+
* server-side and effectively non-expiring from the client's POV).
|
|
80
|
+
*/
|
|
81
|
+
export function jwtExpiryMs(token: string): number | undefined {
|
|
82
|
+
const parts = token.split(".");
|
|
83
|
+
if (parts.length !== 3) return undefined;
|
|
84
|
+
const payload = parts[1];
|
|
85
|
+
if (!payload) return undefined;
|
|
86
|
+
try {
|
|
87
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { exp?: unknown };
|
|
88
|
+
if (typeof decoded.exp !== "number" || !Number.isFinite(decoded.exp)) return undefined;
|
|
89
|
+
return decoded.exp * 1000;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Collect all available auth methods to try in priority order */
|
|
96
|
+
export async function getAvailableAuthMethods(
|
|
97
|
+
authStorage: AuthStorage,
|
|
98
|
+
sessionId: string | undefined,
|
|
99
|
+
options?: PerplexityAuthOptions,
|
|
100
|
+
): Promise<PerplexityAuth[]> {
|
|
101
|
+
const methods: PerplexityAuth[] = [];
|
|
102
|
+
|
|
103
|
+
// 1. Cookies take precedence over OAuth as noted in comments/docs
|
|
104
|
+
const cookies = $env.PERPLEXITY_COOKIES?.trim();
|
|
105
|
+
if (cookies) {
|
|
106
|
+
methods.push({ type: "cookies", cookies });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Perplexity OAuth (session bearer)
|
|
110
|
+
try {
|
|
111
|
+
const access = await authStorage.getOAuthAccess("perplexity", sessionId, options);
|
|
112
|
+
const token = access?.accessToken;
|
|
113
|
+
if (access && token) {
|
|
114
|
+
const jwtExpiry = jwtExpiryMs(token);
|
|
115
|
+
if (jwtExpiry === undefined || jwtExpiry > Date.now() + OAUTH_EXPIRY_BUFFER_MS) {
|
|
116
|
+
methods.push({ type: "oauth", access });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// ignored
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. API key configs (direct, then openrouter)
|
|
124
|
+
const apiConfigs = await getApiConfigs(authStorage, sessionId, options);
|
|
125
|
+
methods.push(...apiConfigs);
|
|
126
|
+
|
|
127
|
+
// 4. Fallback to Perplexity free (anonymous)
|
|
128
|
+
if (methods.length === 0) {
|
|
129
|
+
methods.push({ type: "anonymous" });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return methods;
|
|
133
|
+
}
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
type AuthStorage,
|
|
15
15
|
type Context,
|
|
16
16
|
type FetchImpl,
|
|
17
|
-
type OAuthAccess,
|
|
18
17
|
type Usage,
|
|
19
18
|
withOAuthAccess,
|
|
20
19
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -34,36 +33,19 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
34
33
|
import { dateToAgeSeconds } from "../utils";
|
|
35
34
|
import type { SearchParams } from "./base";
|
|
36
35
|
import { SearchProvider } from "./base";
|
|
36
|
+
import { type ApiConfig, getAvailableAuthMethods } from "./perplexity-auth";
|
|
37
37
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
38
38
|
|
|
39
|
-
const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
|
|
40
|
-
const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
|
|
41
|
-
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
42
39
|
const PERPLEXITY_OAUTH_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
|
43
40
|
|
|
44
41
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
45
42
|
const DEFAULT_TEMPERATURE = 0.2;
|
|
46
43
|
const DEFAULT_NUM_SEARCH_RESULTS = 20;
|
|
47
|
-
const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
48
44
|
const OAUTH_API_VERSION = "2.18";
|
|
49
45
|
const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
|
|
50
46
|
const ANONYMOUS_USER_AGENT =
|
|
51
47
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
|
52
48
|
|
|
53
|
-
type PerplexityAuth =
|
|
54
|
-
| ApiConfig
|
|
55
|
-
| {
|
|
56
|
-
type: "oauth";
|
|
57
|
-
access: OAuthAccess;
|
|
58
|
-
}
|
|
59
|
-
| {
|
|
60
|
-
type: "cookies";
|
|
61
|
-
cookies: string;
|
|
62
|
-
}
|
|
63
|
-
| {
|
|
64
|
-
type: "anonymous";
|
|
65
|
-
};
|
|
66
|
-
|
|
67
49
|
interface PerplexityOAuthStreamMarkdownBlock {
|
|
68
50
|
answer?: string;
|
|
69
51
|
chunks?: string[];
|
|
@@ -289,111 +271,6 @@ export interface PerplexitySearchParams {
|
|
|
289
271
|
fetch?: FetchImpl;
|
|
290
272
|
}
|
|
291
273
|
|
|
292
|
-
interface ApiConfig {
|
|
293
|
-
type: "api_key";
|
|
294
|
-
apiKey: string;
|
|
295
|
-
provider: "perplexity" | "openrouter";
|
|
296
|
-
chatBaseUrl: string;
|
|
297
|
-
responsesBaseUrl: string;
|
|
298
|
-
modelPrefix: string;
|
|
299
|
-
useResponses: boolean;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
|
|
303
|
-
async function getApiConfigs(
|
|
304
|
-
authStorage: AuthStorage,
|
|
305
|
-
sessionId: string | undefined,
|
|
306
|
-
signal: AbortSignal | undefined,
|
|
307
|
-
): Promise<ApiConfig[]> {
|
|
308
|
-
const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
|
|
309
|
-
const configs: ApiConfig[] = [];
|
|
310
|
-
|
|
311
|
-
const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, { signal });
|
|
312
|
-
if (perplexityKey) {
|
|
313
|
-
configs.push({
|
|
314
|
-
type: "api_key",
|
|
315
|
-
apiKey: perplexityKey,
|
|
316
|
-
provider: "perplexity",
|
|
317
|
-
chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
|
|
318
|
-
responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
|
|
319
|
-
modelPrefix: "",
|
|
320
|
-
useResponses,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, { signal });
|
|
325
|
-
if (openrouterKey) {
|
|
326
|
-
configs.push({
|
|
327
|
-
type: "api_key",
|
|
328
|
-
apiKey: openrouterKey,
|
|
329
|
-
provider: "openrouter",
|
|
330
|
-
chatBaseUrl: OPENROUTER_BASE_URL,
|
|
331
|
-
responsesBaseUrl: OPENROUTER_BASE_URL,
|
|
332
|
-
modelPrefix: "perplexity/",
|
|
333
|
-
useResponses,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return configs;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Decode a Perplexity JWT's `exp` claim, in ms. Returns `undefined` when the
|
|
342
|
-
* token has no `exp` (which is the common case — Perplexity sessions are
|
|
343
|
-
* server-side and effectively non-expiring from the client's POV).
|
|
344
|
-
*/
|
|
345
|
-
function jwtExpiryMs(token: string): number | undefined {
|
|
346
|
-
const parts = token.split(".");
|
|
347
|
-
if (parts.length !== 3) return undefined;
|
|
348
|
-
const payload = parts[1];
|
|
349
|
-
if (!payload) return undefined;
|
|
350
|
-
try {
|
|
351
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { exp?: unknown };
|
|
352
|
-
if (typeof decoded.exp !== "number" || !Number.isFinite(decoded.exp)) return undefined;
|
|
353
|
-
return decoded.exp * 1000;
|
|
354
|
-
} catch {
|
|
355
|
-
return undefined;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** Collect all available auth methods to try in priority order */
|
|
360
|
-
async function getAvailableAuthMethods(
|
|
361
|
-
authStorage: AuthStorage,
|
|
362
|
-
sessionId: string | undefined,
|
|
363
|
-
signal: AbortSignal | undefined,
|
|
364
|
-
): Promise<PerplexityAuth[]> {
|
|
365
|
-
const methods: PerplexityAuth[] = [];
|
|
366
|
-
|
|
367
|
-
// 1. Perplexity OAuth & Cookies (same priority - highest)
|
|
368
|
-
try {
|
|
369
|
-
const access = await authStorage.getOAuthAccess("perplexity", sessionId, { signal });
|
|
370
|
-
const token = access?.accessToken;
|
|
371
|
-
if (access && token) {
|
|
372
|
-
const jwtExpiry = jwtExpiryMs(token);
|
|
373
|
-
if (jwtExpiry === undefined || jwtExpiry > Date.now() + OAUTH_EXPIRY_BUFFER_MS) {
|
|
374
|
-
methods.push({ type: "oauth", access });
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
} catch {
|
|
378
|
-
// ignored
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const cookies = $env.PERPLEXITY_COOKIES?.trim();
|
|
382
|
-
if (cookies) {
|
|
383
|
-
methods.push({ type: "cookies", cookies });
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const apiConfigs = await getApiConfigs(authStorage, sessionId, signal);
|
|
387
|
-
methods.push(...apiConfigs);
|
|
388
|
-
|
|
389
|
-
// 5. Fallback to Perplexity free (anonymous)
|
|
390
|
-
if (methods.length === 0) {
|
|
391
|
-
methods.push({ type: "anonymous" });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return methods;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
274
|
interface PerplexityApiStreamMetadata {
|
|
398
275
|
id?: string;
|
|
399
276
|
model?: string;
|
|
@@ -904,7 +781,7 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
|
904
781
|
request.search_recency_filter = params.search_recency_filter;
|
|
905
782
|
}
|
|
906
783
|
|
|
907
|
-
const authMethods = await getAvailableAuthMethods(params.authStorage, params.sessionId, params.signal);
|
|
784
|
+
const authMethods = await getAvailableAuthMethods(params.authStorage, params.sessionId, { signal: params.signal });
|
|
908
785
|
let lastError: unknown;
|
|
909
786
|
|
|
910
787
|
for (const auth of authMethods) {
|