@oh-my-pi/pi-coding-agent 16.1.1 → 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 +50 -0
- package/dist/cli.js +3090 -3115
- package/dist/types/cli/bench-cli.d.ts +2 -1
- package/dist/types/config/model-resolver.d.ts +3 -3
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/main.d.ts +2 -0
- 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/modes/components/assistant-message.d.ts +12 -0
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/sdk.d.ts +19 -2
- package/dist/types/session/agent-storage.d.ts +2 -0
- package/dist/types/session/auth-broker-config.d.ts +34 -6
- package/dist/types/session/history-storage.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +5 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/find.d.ts +0 -2
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/search.d.ts +3 -3
- 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/scripts/measure-prompt-tokens.ts +63 -0
- package/src/cli/bench-cli.ts +64 -3
- package/src/cli/startup-cwd.ts +3 -13
- 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/config/settings-schema.ts +1 -1
- package/src/cursor.ts +1 -1
- package/src/debug/raw-sse-buffer.ts +31 -10
- package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
- package/src/eval/py/prelude.py +1 -1
- package/src/export/html/tool-views.generated.js +1 -1
- package/src/extensibility/extensions/runner.ts +8 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +24 -0
- package/src/main.ts +29 -9
- 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/assistant-message.ts +86 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +87 -9
- package/src/modes/controllers/event-controller.ts +9 -1
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/theme/theme.ts +69 -0
- package/src/prompts/system/personalities/default.md +8 -16
- package/src/prompts/system/system-prompt.md +101 -115
- package/src/prompts/tools/ast-edit.md +10 -12
- package/src/prompts/tools/ast-grep.md +14 -18
- package/src/prompts/tools/bash.md +19 -21
- package/src/prompts/tools/browser.md +24 -24
- package/src/prompts/tools/checkpoint.md +0 -1
- package/src/prompts/tools/debug.md +11 -15
- package/src/prompts/tools/eval.md +27 -27
- package/src/prompts/tools/find.md +6 -10
- package/src/prompts/tools/github.md +11 -15
- package/src/prompts/tools/goal.md +0 -7
- package/src/prompts/tools/inspect-image.md +0 -1
- package/src/prompts/tools/irc.md +15 -24
- package/src/prompts/tools/job.md +5 -8
- package/src/prompts/tools/learn.md +2 -2
- package/src/prompts/tools/lsp.md +27 -30
- package/src/prompts/tools/manage-skill.md +4 -4
- package/src/prompts/tools/read.md +21 -23
- package/src/prompts/tools/replace.md +0 -1
- package/src/prompts/tools/resolve.md +4 -9
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search.md +8 -10
- package/src/prompts/tools/task.md +33 -38
- package/src/prompts/tools/todo.md +14 -18
- package/src/prompts/tools/web-search.md +0 -4
- package/src/prompts/tools/write.md +1 -1
- package/src/sdk.ts +53 -102
- package/src/session/agent-session.ts +25 -2
- package/src/session/agent-storage.ts +14 -0
- package/src/session/auth-broker-config.ts +37 -76
- package/src/session/history-storage.ts +13 -1
- package/src/session/session-history-format.ts +1 -1
- package/src/session/session-manager.ts +33 -6
- package/src/stt/asr-client.ts +2 -7
- package/src/system-prompt.ts +28 -8
- package/src/task/executor.ts +57 -0
- package/src/task/index.ts +15 -1
- package/src/tiny/title-client.ts +2 -7
- package/src/tools/browser.ts +1 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +4 -17
- package/src/tools/image-gen.ts +4 -8
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/render-utils.ts +4 -1
- package/src/tools/search.ts +5 -5
- 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
package/src/tools/image-gen.ts
CHANGED
|
@@ -1572,19 +1572,15 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1572
1572
|
};
|
|
1573
1573
|
|
|
1574
1574
|
export async function getImageGenTools(
|
|
1575
|
-
|
|
1576
|
-
|
|
1575
|
+
_modelRegistry?: ModelRegistry,
|
|
1576
|
+
_activeModel?: Model,
|
|
1577
1577
|
): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>> {
|
|
1578
|
-
const apiKey = await findImageApiKey(modelRegistry, activeModel);
|
|
1579
|
-
if (!apiKey) return [];
|
|
1580
1578
|
return [imageGenTool];
|
|
1581
1579
|
}
|
|
1582
1580
|
|
|
1583
1581
|
export async function getImageGenToolsWithRegistry(
|
|
1584
|
-
|
|
1585
|
-
|
|
1582
|
+
_modelRegistry: ModelRegistry,
|
|
1583
|
+
_activeModel?: Model,
|
|
1586
1584
|
): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>> {
|
|
1587
|
-
const apiKey = await findImageApiKey(modelRegistry, activeModel);
|
|
1588
|
-
if (!apiKey) return [];
|
|
1589
1585
|
return [imageGenTool];
|
|
1590
1586
|
}
|
package/src/tools/memory-edit.ts
CHANGED
|
@@ -7,7 +7,7 @@ const memoryEditSchema = type({
|
|
|
7
7
|
op: type("'update' | 'forget' | 'invalidate'").describe("memory edit operation"),
|
|
8
8
|
id: type("string").describe("memory id from recall output"),
|
|
9
9
|
"content?": type("string").describe("replacement content for update"),
|
|
10
|
-
"importance?": type("number").describe("replacement importance for update
|
|
10
|
+
"importance?": type("number").describe("replacement importance for update (0–1)"),
|
|
11
11
|
"replacement_id?": type("string").describe("replacement memory id for invalidate"),
|
|
12
12
|
});
|
|
13
13
|
|
|
@@ -657,7 +657,10 @@ export function truncateDiffByHunk(
|
|
|
657
657
|
export function shortenPath(filePath: string, homeDir?: string): string {
|
|
658
658
|
const home = homeDir ?? os.homedir();
|
|
659
659
|
if (home && filePath.startsWith(home)) {
|
|
660
|
-
|
|
660
|
+
const suffix = filePath.slice(home.length);
|
|
661
|
+
if (suffix === "" || suffix.startsWith(path.posix.sep) || suffix.startsWith(path.win32.sep)) {
|
|
662
|
+
return `~${suffix.replaceAll(path.win32.sep, path.posix.sep)}`;
|
|
663
|
+
}
|
|
661
664
|
}
|
|
662
665
|
return filePath;
|
|
663
666
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -70,7 +70,7 @@ const searchSchema = type({
|
|
|
70
70
|
.describe(
|
|
71
71
|
'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
|
|
72
72
|
),
|
|
73
|
-
"
|
|
73
|
+
"case?": type("boolean").describe("case-sensitive search"),
|
|
74
74
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
75
75
|
"skip?": type("number")
|
|
76
76
|
.or("null")
|
|
@@ -680,7 +680,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
680
680
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
681
681
|
_toolContext?: AgentToolContext,
|
|
682
682
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
683
|
-
const { pattern, paths: rawPaths,
|
|
683
|
+
const { pattern, paths: rawPaths, case: caseSensitive, gitignore, skip } = params;
|
|
684
684
|
|
|
685
685
|
return untilAborted(signal, async () => {
|
|
686
686
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -763,7 +763,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
763
763
|
}
|
|
764
764
|
const normalizedContextBefore = this.session.settings.get("search.contextBefore");
|
|
765
765
|
const normalizedContextAfter = this.session.settings.get("search.contextAfter");
|
|
766
|
-
const ignoreCase =
|
|
766
|
+
const ignoreCase = !(caseSensitive ?? true);
|
|
767
767
|
const useGitignore = gitignore ?? true;
|
|
768
768
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
769
769
|
const effectiveMultiline = patternHasNewline;
|
|
@@ -1272,7 +1272,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1272
1272
|
interface SearchRenderArgs {
|
|
1273
1273
|
pattern: string;
|
|
1274
1274
|
paths?: string | string[];
|
|
1275
|
-
|
|
1275
|
+
case?: boolean;
|
|
1276
1276
|
gitignore?: boolean;
|
|
1277
1277
|
skip?: number;
|
|
1278
1278
|
}
|
|
@@ -1443,7 +1443,7 @@ export const searchToolRenderer = {
|
|
|
1443
1443
|
const paths = toPathList(args.paths);
|
|
1444
1444
|
const meta: string[] = [];
|
|
1445
1445
|
if (paths.length) meta.push(`in ${paths.join(", ")}`);
|
|
1446
|
-
if (args.
|
|
1446
|
+
if (args.case === false) meta.push("case:insensitive");
|
|
1447
1447
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
1448
1448
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
1449
1449
|
|
package/src/tts/tts-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } fro
|
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import { settings } from "../config/settings";
|
|
5
5
|
import { tinyWorkerEnvOverlay } from "../tiny/title-client";
|
|
6
|
+
import { safeSend } from "../utils/ipc";
|
|
6
7
|
import { isTtsLocalModelKey, type TtsLocalModelKey } from "./models";
|
|
7
8
|
import type { TtsProgressEvent, TtsWorkerInbound, TtsWorkerOutbound } from "./tts-protocol";
|
|
8
9
|
|
|
@@ -245,13 +246,7 @@ export function createTtsSubprocess(): SpawnedSubprocess {
|
|
|
245
246
|
function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
|
|
246
247
|
return {
|
|
247
248
|
send(message) {
|
|
248
|
-
|
|
249
|
-
proc.send(message);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
logger.debug("tts: send to subprocess failed", {
|
|
252
|
-
error: error instanceof Error ? error.message : String(error),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
249
|
+
safeSend(proc, message, "tts");
|
|
255
250
|
},
|
|
256
251
|
onMessage(handler) {
|
|
257
252
|
inbound.add(handler);
|
|
@@ -13,9 +13,19 @@ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
|
|
13
13
|
* with an opaque HTTP 400. Detect those models so the resize pipeline encodes
|
|
14
14
|
* to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
|
|
15
15
|
*/
|
|
16
|
-
export function modelLacksWebpSupport(
|
|
16
|
+
export function modelLacksWebpSupport(
|
|
17
|
+
model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined,
|
|
18
|
+
): boolean {
|
|
17
19
|
if (!model) return false;
|
|
18
|
-
return
|
|
20
|
+
return (
|
|
21
|
+
model.imageInputDecoder === "stb" ||
|
|
22
|
+
model.provider === "ollama" ||
|
|
23
|
+
model.provider === "ollama-cloud" ||
|
|
24
|
+
model.provider === "llama.cpp" ||
|
|
25
|
+
model.provider === "lm-studio" ||
|
|
26
|
+
model.provider === "local-server" ||
|
|
27
|
+
model.api === "ollama-chat"
|
|
28
|
+
);
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
/**
|
package/src/utils/ipc.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow a value to a thenable so a rejection handler can be attached.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the local helper in `mcp/transports/stdio.ts` (kept separate because
|
|
7
|
+
* that copy serves the FileSink stdin-write path and is battle-tested there).
|
|
8
|
+
* This shared copy is the home for the IPC `send()` sites.
|
|
9
|
+
*/
|
|
10
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
11
|
+
return (
|
|
12
|
+
value != null &&
|
|
13
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
14
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Send a message to a Bun subprocess over IPC, neutralizing both the
|
|
20
|
+
* synchronous throw ("cannot be used after the process has exited") and any
|
|
21
|
+
* asynchronous rejection (EPIPE from a pipe that broke between exit being
|
|
22
|
+
* observed and the next `send()`). The dead worker is detected separately via
|
|
23
|
+
* `onExit`/`onError` and respawned or disabled by the owning client; an
|
|
24
|
+
* un-awaited EPIPE rejection must not escape as a fatal unhandled rejection
|
|
25
|
+
* that takes down the whole session. See issue #2997.
|
|
26
|
+
*
|
|
27
|
+
* `label` prefixes the debug log on synchronous failure (e.g. "tts").
|
|
28
|
+
*/
|
|
29
|
+
export function safeSend(proc: { send(message: unknown): unknown }, message: unknown, label: string): void {
|
|
30
|
+
try {
|
|
31
|
+
const result = proc.send(message);
|
|
32
|
+
if (isThenable(result)) result.then(undefined, () => {});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.debug(`${label}: send to subprocess failed`, {
|
|
35
|
+
error: error instanceof Error ? error.message : String(error),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -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) {
|