@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/settings-selector.ts +10 -1
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/theme/theme.ts +4 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +33 -26
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +26 -57
- package/src/task/index.ts +8 -4
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -31
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -246
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* OpenAI Codex Web Search Provider
|
|
3
3
|
*
|
|
4
4
|
* Uses Codex's built-in web_search tool via the Responses API.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Auth is resolved through `AuthStorage.getOAuthAccess("openai-codex")` so the
|
|
6
|
+
* broker is the sole refresh authority — this module never opens a sibling
|
|
7
|
+
* SQLite store, never POSTs the broker sentinel to an OpenAI token endpoint.
|
|
7
8
|
*/
|
|
8
9
|
import * as os from "node:os";
|
|
9
|
-
import { getBundledModels } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import { type AuthStorage, getBundledModels } from "@oh-my-pi/pi-ai";
|
|
10
11
|
import { decodeJwt } from "@oh-my-pi/pi-ai/utils/oauth/openai-codex";
|
|
11
|
-
import { $env,
|
|
12
|
+
import { $env, readSseJson } from "@oh-my-pi/pi-utils";
|
|
12
13
|
import packageJson from "../../../../package.json" with { type: "json" };
|
|
13
|
-
import { AgentStorage } from "../../../session/agent-storage";
|
|
14
14
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
15
15
|
import { SearchProviderError } from "../../../web/search/types";
|
|
16
16
|
import type { SearchParams } from "./base";
|
|
@@ -19,29 +19,48 @@ import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
|
19
19
|
|
|
20
20
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
21
21
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
22
|
-
const FALLBACK_MODEL = "gpt-5
|
|
22
|
+
const FALLBACK_MODEL = "gpt-5.4";
|
|
23
23
|
const DEFAULT_MODEL_PREFERENCES = [
|
|
24
|
-
"gpt-5-codex-mini",
|
|
25
24
|
"gpt-5.4",
|
|
25
|
+
"gpt-5-codex",
|
|
26
|
+
"gpt-5",
|
|
26
27
|
"gpt-5.3-codex",
|
|
27
28
|
"gpt-5.2-codex",
|
|
28
29
|
"gpt-5.1-codex",
|
|
29
|
-
"gpt-5-codex",
|
|
30
|
+
"gpt-5-codex-mini",
|
|
30
31
|
];
|
|
31
32
|
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
32
33
|
const DEFAULT_INSTRUCTIONS =
|
|
33
34
|
"You are a helpful assistant with web search capabilities. Search the web to answer the user's question accurately and cite your sources.";
|
|
34
35
|
|
|
35
|
-
function
|
|
36
|
+
function getConfiguredModel(): string | undefined {
|
|
36
37
|
const configuredModel = $env.PI_CODEX_WEB_SEARCH_MODEL?.trim();
|
|
37
|
-
|
|
38
|
+
return configuredModel ? configuredModel : undefined;
|
|
39
|
+
}
|
|
38
40
|
|
|
41
|
+
function getDefaultModelCandidates(): string[] {
|
|
39
42
|
const bundledModels = getBundledModels("openai-codex");
|
|
40
43
|
const bundledIds = new Set(bundledModels.map(model => model.id));
|
|
41
|
-
const
|
|
42
|
-
|
|
44
|
+
const candidates = DEFAULT_MODEL_PREFERENCES.filter(modelId => bundledIds.has(modelId));
|
|
45
|
+
|
|
46
|
+
if (candidates.length > 0) {
|
|
47
|
+
return candidates;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
const nonMini = bundledModels.find(model => !model.id.includes("mini") && !model.id.includes("spark"));
|
|
44
|
-
|
|
51
|
+
if (nonMini) {
|
|
52
|
+
return [nonMini.id];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return bundledModels[0]?.id ? [bundledModels[0].id] : [FALLBACK_MODEL];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shouldRetryWithNextDefaultModel(error: unknown): boolean {
|
|
59
|
+
if (!(error instanceof SearchProviderError)) return false;
|
|
60
|
+
if (error.provider !== "codex" || error.status !== 400) return false;
|
|
61
|
+
return /model is not supported|requested model is not supported|not supported when using codex with a chatgpt account/i.test(
|
|
62
|
+
error.message,
|
|
63
|
+
);
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
export interface CodexSearchParams {
|
|
@@ -53,15 +72,6 @@ export interface CodexSearchParams {
|
|
|
53
72
|
search_context_size?: "low" | "medium" | "high";
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
/** OAuth credential stored in agent.db */
|
|
57
|
-
interface CodexOAuthCredential {
|
|
58
|
-
type: "oauth";
|
|
59
|
-
access: string;
|
|
60
|
-
refresh?: string;
|
|
61
|
-
expires: number;
|
|
62
|
-
accountId?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
75
|
/** Codex API response structure */
|
|
66
76
|
interface CodexResponseItem {
|
|
67
77
|
type: string;
|
|
@@ -231,7 +241,7 @@ function extractTextSources(text: string): SearchSource[] {
|
|
|
231
241
|
* @param accessToken - JWT access token
|
|
232
242
|
* @returns Account ID string, or null if not found
|
|
233
243
|
*/
|
|
234
|
-
function
|
|
244
|
+
function getAccountIdFromJwt(accessToken: string): string | null {
|
|
235
245
|
const payload = decodeJwt(accessToken);
|
|
236
246
|
const auth = payload?.[JWT_CLAIM_PATH] as { chatgpt_account_id?: string } | undefined;
|
|
237
247
|
const accountId = auth?.chatgpt_account_id;
|
|
@@ -239,43 +249,25 @@ function getAccountId(accessToken: string): string | null {
|
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
252
|
+
* Resolve a Codex bearer + accountId through {@link AuthStorage} — the single
|
|
253
|
+
* refresh authority. Returns `null` when no OAuth credential is configured,
|
|
254
|
+
* when the credential cannot be refreshed (broker error, revoked token, etc.),
|
|
255
|
+
* or when the access token carries no `chatgpt_account_id` claim.
|
|
245
256
|
*/
|
|
246
|
-
async function findCodexAuth(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (credential.type !== "oauth") continue;
|
|
257
|
-
|
|
258
|
-
const oauthCred = credential as CodexOAuthCredential;
|
|
259
|
-
if (!oauthCred.access) continue;
|
|
260
|
-
if (oauthCred.expires <= now + expiryBuffer) continue;
|
|
261
|
-
|
|
262
|
-
const accountId = oauthCred.accountId ?? getAccountId(oauthCred.access);
|
|
263
|
-
if (!accountId) continue;
|
|
264
|
-
|
|
265
|
-
return { accessToken: oauthCred.access, accountId };
|
|
266
|
-
}
|
|
267
|
-
} catch {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return null;
|
|
257
|
+
async function findCodexAuth(
|
|
258
|
+
authStorage: AuthStorage,
|
|
259
|
+
sessionId: string | undefined,
|
|
260
|
+
signal: AbortSignal | undefined,
|
|
261
|
+
): Promise<{ accessToken: string; accountId: string } | null> {
|
|
262
|
+
const access = await authStorage.getOAuthAccess("openai-codex", sessionId, { signal });
|
|
263
|
+
if (!access) return null;
|
|
264
|
+
const accountId = access.accountId ?? getAccountIdFromJwt(access.accessToken);
|
|
265
|
+
if (!accountId) return null;
|
|
266
|
+
return { accessToken: access.accessToken, accountId };
|
|
272
267
|
}
|
|
273
268
|
|
|
274
269
|
/**
|
|
275
270
|
* Builds HTTP headers for Codex API requests.
|
|
276
|
-
* @param accessToken - OAuth access token
|
|
277
|
-
* @param accountId - ChatGPT account ID
|
|
278
|
-
* @returns Headers object for fetch requests
|
|
279
271
|
*/
|
|
280
272
|
function buildCodexHeaders(accessToken: string, accountId: string): Record<string, string> {
|
|
281
273
|
return {
|
|
@@ -291,17 +283,19 @@ function buildCodexHeaders(accessToken: string, accountId: string): Record<strin
|
|
|
291
283
|
|
|
292
284
|
/**
|
|
293
285
|
* Calls the Codex Responses API with web search tool enabled.
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* @param options - Search options including system prompt and context size
|
|
298
|
-
* @returns Parsed response with answer, sources, and usage
|
|
299
|
-
* @throws {SearchProviderError} If the API request fails
|
|
286
|
+
* The caller provides the exact model id to send; retry / fallback policy
|
|
287
|
+
* lives one layer up in `searchCodex()` so we can distinguish explicit user
|
|
288
|
+
* overrides from the default ChatGPT-account model-selection path.
|
|
300
289
|
*/
|
|
301
290
|
async function callCodexSearch(
|
|
302
291
|
auth: { accessToken: string; accountId: string },
|
|
303
292
|
query: string,
|
|
304
|
-
options: {
|
|
293
|
+
options: {
|
|
294
|
+
signal?: AbortSignal;
|
|
295
|
+
systemPrompt?: string;
|
|
296
|
+
searchContextSize?: "low" | "medium" | "high";
|
|
297
|
+
modelId: string;
|
|
298
|
+
},
|
|
305
299
|
): Promise<{
|
|
306
300
|
answer: string;
|
|
307
301
|
sources: SearchSource[];
|
|
@@ -312,7 +306,7 @@ async function callCodexSearch(
|
|
|
312
306
|
const url = `${CODEX_BASE_URL}${CODEX_RESPONSES_PATH}`;
|
|
313
307
|
const headers = buildCodexHeaders(auth.accessToken, auth.accountId);
|
|
314
308
|
|
|
315
|
-
const requestedModel =
|
|
309
|
+
const requestedModel = options.modelId;
|
|
316
310
|
|
|
317
311
|
const body: Record<string, unknown> = {
|
|
318
312
|
model: requestedModel,
|
|
@@ -457,29 +451,67 @@ async function callCodexSearch(
|
|
|
457
451
|
|
|
458
452
|
/**
|
|
459
453
|
* Executes a web search using OpenAI Codex's built-in web search tool.
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
454
|
+
*
|
|
455
|
+
* Default-model behavior:
|
|
456
|
+
* - If `PI_CODEX_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
|
|
457
|
+
* upstream error verbatim.
|
|
458
|
+
* - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.4, GPT-5
|
|
459
|
+
* Codex, GPT-5, …) and retry the next candidate only when Codex returns the
|
|
460
|
+
* known 400 "model is not supported" family. This avoids selecting
|
|
461
|
+
* `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
|
|
464
462
|
*/
|
|
465
|
-
export async function searchCodex(params:
|
|
466
|
-
const auth = await findCodexAuth();
|
|
463
|
+
export async function searchCodex(params: SearchParams): Promise<SearchResponse> {
|
|
464
|
+
const auth = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
|
|
467
465
|
if (!auth) {
|
|
468
466
|
throw new Error(
|
|
469
467
|
"No Codex OAuth credentials found. Login with 'omp /login openai-codex' to enable Codex web search.",
|
|
470
468
|
);
|
|
471
469
|
}
|
|
472
470
|
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
471
|
+
const configuredModel = getConfiguredModel();
|
|
472
|
+
const modelCandidates = configuredModel ? [configuredModel] : getDefaultModelCandidates();
|
|
473
|
+
|
|
474
|
+
let result:
|
|
475
|
+
| {
|
|
476
|
+
answer: string;
|
|
477
|
+
sources: SearchSource[];
|
|
478
|
+
model: string;
|
|
479
|
+
requestId: string;
|
|
480
|
+
usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
|
|
481
|
+
}
|
|
482
|
+
| undefined;
|
|
483
|
+
let lastError: unknown;
|
|
484
|
+
|
|
485
|
+
for (let index = 0; index < modelCandidates.length; index += 1) {
|
|
486
|
+
const modelId = modelCandidates[index];
|
|
487
|
+
if (!modelId) continue;
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
result = await callCodexSearch(auth, params.query, {
|
|
491
|
+
signal: params.signal,
|
|
492
|
+
systemPrompt: params.systemPrompt,
|
|
493
|
+
searchContextSize: "high",
|
|
494
|
+
modelId,
|
|
495
|
+
});
|
|
496
|
+
break;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
lastError = error;
|
|
499
|
+
const isLastCandidate = index === modelCandidates.length - 1;
|
|
500
|
+
if (configuredModel || isLastCandidate || !shouldRetryWithNextDefaultModel(error)) {
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!result) {
|
|
507
|
+
throw lastError ?? new Error("Codex search failed without returning a result");
|
|
508
|
+
}
|
|
477
509
|
|
|
478
510
|
let sources = result.sources;
|
|
479
511
|
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
sources = sources.slice(0,
|
|
512
|
+
const numResults = params.numSearchResults ?? params.limit;
|
|
513
|
+
if (numResults && sources.length > numResults) {
|
|
514
|
+
sources = sources.slice(0, numResults);
|
|
483
515
|
}
|
|
484
516
|
|
|
485
517
|
return {
|
|
@@ -500,28 +532,25 @@ export async function searchCodex(params: CodexSearchParams): Promise<SearchResp
|
|
|
500
532
|
|
|
501
533
|
/**
|
|
502
534
|
* Checks if Codex web search is available.
|
|
503
|
-
* @returns True if valid OAuth credentials exist for openai-codex
|
|
504
535
|
*/
|
|
505
|
-
export async function hasCodexSearch(): Promise<boolean> {
|
|
506
|
-
|
|
507
|
-
|
|
536
|
+
export async function hasCodexSearch(authStorage: AuthStorage): Promise<boolean> {
|
|
537
|
+
// `isAvailable` runs before every request — keep the probe cheap.
|
|
538
|
+
// `hasOAuth(...)` is a synchronous in-memory check that returns true as soon
|
|
539
|
+
// as a Codex OAuth credential is loaded, without driving the refresh
|
|
540
|
+
// pipeline. The actual refresh happens lazily in `searchCodex`.
|
|
541
|
+
return authStorage.hasOAuth("openai-codex");
|
|
508
542
|
}
|
|
509
543
|
|
|
510
544
|
/** Search provider for OpenAI Codex web search. */
|
|
511
545
|
export class CodexProvider extends SearchProvider {
|
|
512
546
|
readonly id = "codex";
|
|
513
|
-
readonly label = "
|
|
547
|
+
readonly label = "OpenAI";
|
|
514
548
|
|
|
515
|
-
isAvailable(): Promise<boolean> {
|
|
516
|
-
return
|
|
549
|
+
isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean {
|
|
550
|
+
return hasCodexSearch(authStorage);
|
|
517
551
|
}
|
|
518
552
|
|
|
519
553
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
520
|
-
return searchCodex(
|
|
521
|
-
signal: params.signal,
|
|
522
|
-
query: params.query,
|
|
523
|
-
system_prompt: params.systemPrompt,
|
|
524
|
-
num_results: params.numSearchResults ?? params.limit,
|
|
525
|
-
});
|
|
554
|
+
return searchCodex(params);
|
|
526
555
|
}
|
|
527
556
|
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* Requests per-result summaries via `contents.summary` and synthesizes
|
|
7
7
|
* them into a combined `answer` string on the SearchResponse.
|
|
8
8
|
*/
|
|
9
|
-
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { settings } from "../../../config/settings";
|
|
11
11
|
import { callExaTool, findApiKey, isSearchResponse } from "../../../exa/mcp-client";
|
|
12
|
+
|
|
12
13
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
13
14
|
import { SearchProviderError } from "../../../web/search/types";
|
|
14
15
|
import { dateToAgeSeconds } from "../utils";
|
|
@@ -249,7 +250,7 @@ export class ExaProvider extends SearchProvider {
|
|
|
249
250
|
readonly id = "exa";
|
|
250
251
|
readonly label = "Exa";
|
|
251
252
|
|
|
252
|
-
isAvailable(): boolean {
|
|
253
|
+
isAvailable(_authStorage: AuthStorage): boolean {
|
|
253
254
|
try {
|
|
254
255
|
if (settings.get("exa.enabled") === false || settings.get("exa.enableSearch") === false) {
|
|
255
256
|
return false;
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* Google Gemini Web Search Provider
|
|
3
3
|
*
|
|
4
4
|
* Uses Gemini's Google Search grounding via Cloud Code Assist API.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Auth is resolved through `AuthStorage.getOAuthAccess(...)` for both
|
|
6
|
+
* `google-gemini-cli` (stable prod) and `google-antigravity` (daily sandbox)
|
|
7
|
+
* — the broker is the sole refresh authority, so this module never opens a
|
|
8
|
+
* sibling SQLite store and never POSTs the broker sentinel to a Google token
|
|
9
|
+
* endpoint.
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
import {
|
|
12
|
+
ANTIGRAVITY_SYSTEM_INSTRUCTION,
|
|
13
|
+
type AuthStorage,
|
|
14
|
+
getAntigravityUserAgent,
|
|
15
|
+
getGeminiCliHeaders,
|
|
16
|
+
} from "@oh-my-pi/pi-ai";
|
|
17
|
+
import { fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
12
18
|
|
|
13
|
-
import { AgentStorage } from "../../../session/agent-storage";
|
|
14
19
|
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
15
20
|
import { SearchProviderError } from "../../../web/search/types";
|
|
16
21
|
import type { SearchParams } from "./base";
|
|
@@ -26,6 +31,9 @@ const MAX_RETRIES = 3;
|
|
|
26
31
|
const BASE_DELAY_MS = 1000;
|
|
27
32
|
const RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
|
28
33
|
|
|
34
|
+
const GEMINI_PROVIDERS = ["google-gemini-cli", "google-antigravity"] as const;
|
|
35
|
+
type GeminiProviderId = (typeof GEMINI_PROVIDERS)[number];
|
|
36
|
+
|
|
29
37
|
interface GeminiToolParams {
|
|
30
38
|
google_search?: Record<string, unknown>;
|
|
31
39
|
code_execution?: Record<string, unknown>;
|
|
@@ -41,6 +49,8 @@ export interface GeminiSearchParams extends GeminiToolParams {
|
|
|
41
49
|
/** Sampling temperature (0–1). Lower = more focused/factual. */
|
|
42
50
|
temperature?: number;
|
|
43
51
|
signal?: AbortSignal;
|
|
52
|
+
authStorage: AuthStorage;
|
|
53
|
+
sessionId?: string;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<string, Record<string, unknown>>> {
|
|
@@ -54,131 +64,40 @@ export function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<
|
|
|
54
64
|
return tools;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
/**
|
|
58
|
-
interface GeminiOAuthCredential {
|
|
59
|
-
type: "oauth";
|
|
60
|
-
access: string;
|
|
61
|
-
refresh?: string;
|
|
62
|
-
expires: number;
|
|
63
|
-
projectId?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Auth info for Gemini API requests */
|
|
67
|
+
/** Resolved auth for a Gemini API request. */
|
|
67
68
|
interface GeminiAuth {
|
|
68
69
|
accessToken: string;
|
|
69
|
-
refreshToken?: string;
|
|
70
70
|
projectId: string;
|
|
71
71
|
isAntigravity: boolean;
|
|
72
|
-
storage: AgentStorage;
|
|
73
|
-
credentialId: number;
|
|
74
|
-
credential: GeminiOAuthCredential;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function refreshGeminiAuth(auth: GeminiAuth): Promise<boolean> {
|
|
78
|
-
if (!auth.refreshToken) return false;
|
|
79
|
-
try {
|
|
80
|
-
const refreshed = auth.isAntigravity
|
|
81
|
-
? await refreshAntigravityToken(auth.refreshToken, auth.projectId)
|
|
82
|
-
: await refreshGoogleCloudToken(auth.refreshToken, auth.projectId);
|
|
83
|
-
auth.accessToken = refreshed.access;
|
|
84
|
-
auth.refreshToken = refreshed.refresh ?? auth.refreshToken;
|
|
85
|
-
auth.storage.updateAuthCredential(auth.credentialId, {
|
|
86
|
-
...auth.credential,
|
|
87
|
-
access: auth.accessToken,
|
|
88
|
-
refresh: auth.refreshToken,
|
|
89
|
-
expires: refreshed.expires,
|
|
90
|
-
});
|
|
91
|
-
auth.credential.access = auth.accessToken;
|
|
92
|
-
auth.credential.refresh = auth.refreshToken;
|
|
93
|
-
auth.credential.expires = refreshed.expires;
|
|
94
|
-
return true;
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
72
|
}
|
|
99
73
|
|
|
100
74
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* @
|
|
75
|
+
* Walks the configured Gemini OAuth providers in deterministic order and
|
|
76
|
+
* returns the first one that yields a usable access token + projectId via
|
|
77
|
+
* {@link AuthStorage.getOAuthAccess}. AuthStorage handles refresh + broker
|
|
78
|
+
* routing internally; this helper never touches refresh tokens directly.
|
|
104
79
|
*/
|
|
105
|
-
export async function findGeminiAuth(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const record of records) {
|
|
119
|
-
const credential = record.credential;
|
|
120
|
-
if (credential.type !== "oauth") continue;
|
|
121
|
-
|
|
122
|
-
const oauthCred = credential as GeminiOAuthCredential;
|
|
123
|
-
if (!oauthCred.access) continue;
|
|
124
|
-
|
|
125
|
-
// Get projectId from credential
|
|
126
|
-
const projectId = oauthCred.projectId;
|
|
127
|
-
if (!projectId) continue;
|
|
128
|
-
|
|
129
|
-
// Check if token is expired (or about to expire)
|
|
130
|
-
if (oauthCred.expires <= now + expiryBuffer) {
|
|
131
|
-
// Try to refresh if we have a refresh token
|
|
132
|
-
if (oauthCred.refresh) {
|
|
133
|
-
try {
|
|
134
|
-
const refreshed =
|
|
135
|
-
provider === "google-antigravity"
|
|
136
|
-
? await refreshAntigravityToken(oauthCred.refresh, projectId)
|
|
137
|
-
: await refreshGoogleCloudToken(oauthCred.refresh, projectId);
|
|
138
|
-
// Update the credential in storage
|
|
139
|
-
const updated = {
|
|
140
|
-
...oauthCred,
|
|
141
|
-
access: refreshed.access,
|
|
142
|
-
refresh: refreshed.refresh ?? oauthCred.refresh,
|
|
143
|
-
expires: refreshed.expires,
|
|
144
|
-
};
|
|
145
|
-
storage.updateAuthCredential(record.id, updated);
|
|
146
|
-
return {
|
|
147
|
-
accessToken: refreshed.access,
|
|
148
|
-
refreshToken: refreshed.refresh ?? oauthCred.refresh,
|
|
149
|
-
projectId,
|
|
150
|
-
isAntigravity: provider === "google-antigravity",
|
|
151
|
-
storage,
|
|
152
|
-
credentialId: record.id,
|
|
153
|
-
credential: updated,
|
|
154
|
-
};
|
|
155
|
-
} catch {
|
|
156
|
-
// Refresh failed, skip this credential
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// No refresh token or refresh failed
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
accessToken: oauthCred.access,
|
|
166
|
-
refreshToken: oauthCred.refresh,
|
|
167
|
-
projectId,
|
|
168
|
-
isAntigravity: provider === "google-antigravity",
|
|
169
|
-
storage,
|
|
170
|
-
credentialId: record.id,
|
|
171
|
-
credential: oauthCred,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
80
|
+
export async function findGeminiAuth(
|
|
81
|
+
authStorage: AuthStorage,
|
|
82
|
+
sessionId: string | undefined,
|
|
83
|
+
signal: AbortSignal | undefined,
|
|
84
|
+
): Promise<GeminiAuth | null> {
|
|
85
|
+
for (const provider of GEMINI_PROVIDERS) {
|
|
86
|
+
const access = await authStorage.getOAuthAccess(provider, sessionId, { signal });
|
|
87
|
+
if (!access?.accessToken || !access.projectId) continue;
|
|
88
|
+
return {
|
|
89
|
+
accessToken: access.accessToken,
|
|
90
|
+
projectId: access.projectId,
|
|
91
|
+
isAntigravity: provider === "google-antigravity",
|
|
92
|
+
};
|
|
177
93
|
}
|
|
178
|
-
|
|
179
94
|
return null;
|
|
180
95
|
}
|
|
181
96
|
|
|
97
|
+
function hasGeminiOAuth(authStorage: AuthStorage): boolean {
|
|
98
|
+
return GEMINI_PROVIDERS.some((provider: GeminiProviderId) => authStorage.hasOAuth(provider));
|
|
99
|
+
}
|
|
100
|
+
|
|
182
101
|
/** Cloud Code Assist API response types */
|
|
183
102
|
interface GeminiGroundingChunk {
|
|
184
103
|
web?: {
|
|
@@ -224,20 +143,20 @@ interface CloudCodeResponseChunk {
|
|
|
224
143
|
|
|
225
144
|
/**
|
|
226
145
|
* Calls the Cloud Code Assist API with Google Search grounding enabled.
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
146
|
+
*
|
|
147
|
+
* If a request returns a refreshable auth failure (401/403/auth-flavoured 400),
|
|
148
|
+
* we ask AuthStorage to invalidate + refresh the credential and retry once.
|
|
149
|
+
* Provider-direct refresh helpers are intentionally not used: AuthStorage owns
|
|
150
|
+
* the single-flight refresh and broker round-trip.
|
|
232
151
|
*/
|
|
233
152
|
async function callGeminiSearch(
|
|
234
153
|
auth: GeminiAuth,
|
|
235
154
|
query: string,
|
|
236
|
-
systemPrompt
|
|
237
|
-
maxOutputTokens
|
|
238
|
-
temperature
|
|
239
|
-
toolParams: GeminiToolParams
|
|
240
|
-
signal
|
|
155
|
+
systemPrompt: string | undefined,
|
|
156
|
+
maxOutputTokens: number | undefined,
|
|
157
|
+
temperature: number | undefined,
|
|
158
|
+
toolParams: GeminiToolParams,
|
|
159
|
+
signal: AbortSignal | undefined,
|
|
241
160
|
): Promise<{
|
|
242
161
|
answer: string;
|
|
243
162
|
sources: SearchSource[];
|
|
@@ -316,29 +235,13 @@ async function callGeminiSearch(
|
|
|
316
235
|
const urlFor = (attempt: number) =>
|
|
317
236
|
`${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
|
|
318
237
|
|
|
319
|
-
|
|
238
|
+
const response = await fetchWithRetry(urlFor, {
|
|
320
239
|
...buildInit(),
|
|
321
240
|
maxAttempts: MAX_RETRIES + 1,
|
|
322
241
|
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
323
242
|
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
324
243
|
});
|
|
325
244
|
|
|
326
|
-
if (!response.ok) {
|
|
327
|
-
const errorText = await response.clone().text();
|
|
328
|
-
const canRefreshAuth =
|
|
329
|
-
response.status === 401 ||
|
|
330
|
-
response.status === 403 ||
|
|
331
|
-
(response.status === 400 && /api key not valid|invalid credentials|invalid authentication/i.test(errorText));
|
|
332
|
-
if (canRefreshAuth && (await refreshGeminiAuth(auth))) {
|
|
333
|
-
response = await fetchWithRetry(urlFor, {
|
|
334
|
-
...buildInit(),
|
|
335
|
-
maxAttempts: MAX_RETRIES + 1,
|
|
336
|
-
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
337
|
-
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
245
|
if (!response.ok) {
|
|
343
246
|
const errorText = await response.text();
|
|
344
247
|
const classified = classifyProviderHttpError("gemini", response.status, errorText);
|
|
@@ -482,13 +385,9 @@ async function callGeminiSearch(
|
|
|
482
385
|
|
|
483
386
|
/**
|
|
484
387
|
* Executes a web search using Google Gemini with Google Search grounding.
|
|
485
|
-
* Requires OAuth credentials stored in agent.db for provider "google-gemini-cli" or "google-antigravity".
|
|
486
|
-
* @param params - Search parameters including query and optional settings
|
|
487
|
-
* @returns Search response with synthesized answer, sources, and citations
|
|
488
|
-
* @throws {Error} If no Gemini OAuth credentials are configured
|
|
489
388
|
*/
|
|
490
389
|
export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
|
|
491
|
-
const auth = await findGeminiAuth();
|
|
390
|
+
const auth = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
|
|
492
391
|
if (!auth) {
|
|
493
392
|
throw new Error(
|
|
494
393
|
"No Gemini OAuth credentials found. Login with 'omp /login google-gemini-cli' or 'omp /login google-antigravity' to enable Gemini web search.",
|
|
@@ -511,7 +410,6 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
|
|
|
511
410
|
|
|
512
411
|
let sources = result.sources;
|
|
513
412
|
|
|
514
|
-
// Apply num_results limit if specified
|
|
515
413
|
if (params.num_results && sources.length > params.num_results) {
|
|
516
414
|
sources = sources.slice(0, params.num_results);
|
|
517
415
|
}
|
|
@@ -532,8 +430,11 @@ export class GeminiProvider extends SearchProvider {
|
|
|
532
430
|
readonly id = "gemini";
|
|
533
431
|
readonly label = "Gemini";
|
|
534
432
|
|
|
535
|
-
isAvailable() {
|
|
536
|
-
|
|
433
|
+
isAvailable(authStorage: AuthStorage): boolean {
|
|
434
|
+
// Cheap, in-memory check — avoids driving the refresh pipeline during
|
|
435
|
+
// the provider-chain probe. `searchGemini` calls `getOAuthAccess` which
|
|
436
|
+
// will refresh lazily on the actual request.
|
|
437
|
+
return hasGeminiOAuth(authStorage);
|
|
537
438
|
}
|
|
538
439
|
|
|
539
440
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
@@ -547,6 +448,8 @@ export class GeminiProvider extends SearchProvider {
|
|
|
547
448
|
code_execution: params.codeExecution,
|
|
548
449
|
url_context: params.urlContext,
|
|
549
450
|
signal: params.signal,
|
|
451
|
+
authStorage: params.authStorage,
|
|
452
|
+
sessionId: params.sessionId,
|
|
550
453
|
});
|
|
551
454
|
}
|
|
552
455
|
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* cleaned content.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
10
10
|
import { SearchProviderError } from "../../../web/search/types";
|
|
11
11
|
import type { SearchParams } from "./base";
|
|
12
12
|
import { SearchProvider } from "./base";
|
|
13
|
-
import { classifyProviderHttpError,
|
|
13
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
14
14
|
|
|
15
15
|
const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
16
16
|
|
|
@@ -87,8 +87,8 @@ export class JinaProvider extends SearchProvider {
|
|
|
87
87
|
readonly id = "jina";
|
|
88
88
|
readonly label = "Jina";
|
|
89
89
|
|
|
90
|
-
isAvailable() {
|
|
91
|
-
return
|
|
90
|
+
isAvailable(_authStorage: AuthStorage): boolean {
|
|
91
|
+
return !!findApiKey();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
search(params: SearchParams): Promise<SearchResponse> {
|