@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -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/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/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 +17 -23
- 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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// The `label`/`id` metadata is kept inline so callers needing a display name
|
|
9
9
|
// (error formatting, UI listings) do not force a load.
|
|
10
10
|
|
|
11
|
+
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
11
12
|
import type { SearchProvider } from "./providers/base";
|
|
12
13
|
import type { SearchProviderId } from "./types";
|
|
13
14
|
|
|
@@ -64,7 +65,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
64
65
|
},
|
|
65
66
|
codex: {
|
|
66
67
|
id: "codex",
|
|
67
|
-
label: "
|
|
68
|
+
label: "OpenAI",
|
|
68
69
|
load: async () => new (await import("./providers/codex")).CodexProvider(),
|
|
69
70
|
},
|
|
70
71
|
tavily: {
|
|
@@ -148,13 +149,14 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
|
|
|
148
149
|
* is walked, so unconfigured providers never pay the load cost.
|
|
149
150
|
*/
|
|
150
151
|
export async function resolveProviderChain(
|
|
152
|
+
authStorage: AuthStorage,
|
|
151
153
|
preferredProvider: SearchProviderId | "auto" = preferredProvId,
|
|
152
154
|
): Promise<SearchProvider[]> {
|
|
153
155
|
const providers: SearchProvider[] = [];
|
|
154
156
|
|
|
155
157
|
if (preferredProvider !== "auto") {
|
|
156
158
|
const provider = await getSearchProvider(preferredProvider);
|
|
157
|
-
if (await provider.isAvailable()) {
|
|
159
|
+
if (await provider.isAvailable(authStorage)) {
|
|
158
160
|
providers.push(provider);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
@@ -162,7 +164,7 @@ export async function resolveProviderChain(
|
|
|
162
164
|
for (const id of SEARCH_PROVIDER_ORDER) {
|
|
163
165
|
if (id === preferredProvider) continue;
|
|
164
166
|
const provider = await getSearchProvider(id);
|
|
165
|
-
if (await provider.isAvailable()) {
|
|
167
|
+
if (await provider.isAvailable(authStorage)) {
|
|
166
168
|
providers.push(provider);
|
|
167
169
|
}
|
|
168
170
|
}
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import {
|
|
8
8
|
type AnthropicAuthConfig,
|
|
9
9
|
type AnthropicSystemBlock,
|
|
10
|
+
type AuthStorage,
|
|
11
|
+
buildAnthropicAuthConfig,
|
|
10
12
|
buildAnthropicSearchHeaders,
|
|
11
13
|
buildAnthropicSystemBlocks,
|
|
12
14
|
buildAnthropicUrl,
|
|
13
|
-
findAnthropicAuth,
|
|
14
15
|
stripClaudeToolPrefix,
|
|
15
16
|
} from "@oh-my-pi/pi-ai";
|
|
16
17
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
@@ -34,9 +35,7 @@ export interface AnthropicSearchParams {
|
|
|
34
35
|
query: string;
|
|
35
36
|
system_prompt?: string;
|
|
36
37
|
num_results?: number;
|
|
37
|
-
/** Maximum output tokens. Defaults to 4096. */
|
|
38
38
|
max_tokens?: number;
|
|
39
|
-
/** Sampling temperature (0–1). Lower = more focused/factual. */
|
|
40
39
|
temperature?: number;
|
|
41
40
|
signal?: AbortSignal;
|
|
42
41
|
}
|
|
@@ -242,30 +241,47 @@ function parseResponse(response: AnthropicApiResponse): SearchResponse {
|
|
|
242
241
|
* @returns Search response with synthesized answer, sources, and citations
|
|
243
242
|
* @throws {Error} If no Anthropic credentials are configured
|
|
244
243
|
*/
|
|
245
|
-
export async function searchAnthropic(
|
|
246
|
-
|
|
244
|
+
export async function searchAnthropic(
|
|
245
|
+
params: SearchParams | AnthropicSearchParams,
|
|
246
|
+
_legacyStorage?: unknown,
|
|
247
|
+
): Promise<SearchResponse> {
|
|
248
|
+
const searchApiKey = $env.ANTHROPIC_SEARCH_API_KEY;
|
|
249
|
+
const searchBaseUrl = $env.ANTHROPIC_SEARCH_BASE_URL;
|
|
250
|
+
let auth: AnthropicAuthConfig | undefined;
|
|
251
|
+
|
|
252
|
+
if (searchApiKey) {
|
|
253
|
+
auth = buildAnthropicAuthConfig(searchApiKey, searchBaseUrl);
|
|
254
|
+
} else if ("authStorage" in params) {
|
|
255
|
+
const apiKey = await params.authStorage.getApiKey("anthropic", params.sessionId, {
|
|
256
|
+
signal: params.signal,
|
|
257
|
+
});
|
|
258
|
+
if (apiKey) auth = buildAnthropicAuthConfig(apiKey);
|
|
259
|
+
}
|
|
260
|
+
|
|
247
261
|
if (!auth) {
|
|
248
262
|
throw new Error(
|
|
249
|
-
"No Anthropic credentials found. Set ANTHROPIC_API_KEY or configure OAuth
|
|
263
|
+
"No Anthropic credentials found. Set ANTHROPIC_SEARCH_API_KEY or ANTHROPIC_API_KEY, or configure Anthropic OAuth.",
|
|
250
264
|
);
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
const model = getModel();
|
|
268
|
+
const systemPrompt = "authStorage" in params ? params.systemPrompt : params.system_prompt;
|
|
269
|
+
const maxTokens = "authStorage" in params ? params.maxOutputTokens : params.max_tokens;
|
|
254
270
|
const response = await callSearch(
|
|
255
271
|
auth,
|
|
256
272
|
model,
|
|
257
273
|
params.query,
|
|
258
|
-
|
|
259
|
-
|
|
274
|
+
systemPrompt,
|
|
275
|
+
maxTokens,
|
|
260
276
|
params.temperature,
|
|
261
277
|
params.signal,
|
|
262
278
|
);
|
|
263
279
|
|
|
264
280
|
const result = parseResponse(response);
|
|
265
281
|
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
result.sources = result.sources.slice(0,
|
|
282
|
+
const numResults = "authStorage" in params ? (params.numSearchResults ?? params.limit) : params.num_results;
|
|
283
|
+
if (numResults && result.sources.length > numResults) {
|
|
284
|
+
result.sources = result.sources.slice(0, numResults);
|
|
269
285
|
}
|
|
270
286
|
|
|
271
287
|
return result;
|
|
@@ -276,18 +292,11 @@ export class AnthropicProvider extends SearchProvider {
|
|
|
276
292
|
readonly id = "anthropic";
|
|
277
293
|
readonly label = "Anthropic";
|
|
278
294
|
|
|
279
|
-
isAvailable() {
|
|
280
|
-
return
|
|
295
|
+
isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean {
|
|
296
|
+
return Boolean($env.ANTHROPIC_SEARCH_API_KEY) || authStorage.hasAuth("anthropic");
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
284
|
-
return searchAnthropic(
|
|
285
|
-
query: params.query,
|
|
286
|
-
system_prompt: params.systemPrompt,
|
|
287
|
-
num_results: params.numSearchResults ?? params.limit,
|
|
288
|
-
max_tokens: params.maxOutputTokens,
|
|
289
|
-
temperature: params.temperature,
|
|
290
|
-
signal: params.signal,
|
|
291
|
-
});
|
|
300
|
+
return searchAnthropic(params);
|
|
292
301
|
}
|
|
293
302
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
1
2
|
import type { SearchProviderId, SearchResponse } from "../types";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Shared web search parameters passed to providers.
|
|
6
|
+
*
|
|
7
|
+
* `authStorage` is the **only** credential source providers may consult.
|
|
8
|
+
* Opening a sibling SQLite handle or calling provider-direct refresh helpers
|
|
9
|
+
* (e.g. `refreshOpenAICodexToken`, `refreshGoogleCloudToken`) is prohibited:
|
|
10
|
+
* it races the broker's per-credential refresh and POSTs the broker sentinel
|
|
11
|
+
* (`REMOTE_REFRESH_SENTINEL`) to the upstream token endpoint, which classifies
|
|
12
|
+
* as `invalid_grant` and disables the row.
|
|
13
|
+
*/
|
|
4
14
|
export interface SearchParams {
|
|
5
15
|
query: string;
|
|
6
16
|
limit?: number;
|
|
@@ -26,6 +36,20 @@ export interface SearchParams {
|
|
|
26
36
|
googleSearch?: Record<string, unknown>;
|
|
27
37
|
codeExecution?: Record<string, unknown>;
|
|
28
38
|
urlContext?: Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* The single source of truth for credentials. Providers MUST consult this
|
|
41
|
+
* handle exclusively (`getApiKey` for bearer-style auth, `getOAuthAccess`
|
|
42
|
+
* when identity metadata is required). Do not open `AgentStorage` or any
|
|
43
|
+
* `AuthCredentialStore` directly — that bypasses the broker pipeline and
|
|
44
|
+
* the per-credential single-flight refresh.
|
|
45
|
+
*/
|
|
46
|
+
authStorage: AuthStorage;
|
|
47
|
+
/**
|
|
48
|
+
* Optional session id used as the round-robin / sticky key when selecting
|
|
49
|
+
* among multiple credentials for the same provider. Pass through from the
|
|
50
|
+
* caller's agent session when available; otherwise omit.
|
|
51
|
+
*/
|
|
52
|
+
sessionId?: string;
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
/** Base class for web search providers. */
|
|
@@ -33,6 +57,15 @@ export abstract class SearchProvider {
|
|
|
33
57
|
abstract readonly id: SearchProviderId;
|
|
34
58
|
abstract readonly label: string;
|
|
35
59
|
|
|
36
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Indicates whether this provider has the credentials/config it needs to
|
|
62
|
+
* service a request right now. Implementations consult the passed
|
|
63
|
+
* {@link AuthStorage} — never a sibling store.
|
|
64
|
+
*/
|
|
65
|
+
abstract isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute a search. Credentials MUST be resolved through `params.authStorage`.
|
|
69
|
+
*/
|
|
37
70
|
abstract search(params: SearchParams): Promise<SearchResponse>;
|
|
38
71
|
}
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* Calls Brave's web search REST API and maps results into the unified
|
|
5
5
|
* SearchResponse shape used by the web search tool.
|
|
6
6
|
*/
|
|
7
|
-
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
9
9
|
import { SearchProviderError } from "../../../web/search/types";
|
|
10
10
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
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 BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
|
|
16
16
|
const DEFAULT_NUM_RESULTS = 10;
|
|
@@ -134,8 +134,8 @@ export class BraveProvider extends SearchProvider {
|
|
|
134
134
|
readonly id = "brave";
|
|
135
135
|
readonly label = "Brave";
|
|
136
136
|
|
|
137
|
-
isAvailable() {
|
|
138
|
-
return
|
|
137
|
+
isAvailable(_authStorage: AuthStorage): boolean {
|
|
138
|
+
return !!findApiKey();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
@@ -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;
|