@gajae-code/coding-agent 0.15.5 → 0.15.6
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 +26 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +2 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +96 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +15 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -7
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +40 -5
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/config/autorouting-tier-map.ts +32 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +51 -9
- package/src/config/model-registry.ts +1021 -122
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +5 -4
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +772 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
package/src/tools/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
} from "../session/agent-session";
|
|
18
18
|
import type { ArtifactManager } from "../session/artifacts";
|
|
19
19
|
import type { ClientBridge } from "../session/client-bridge";
|
|
20
|
+
import type { FoldAdapter } from "../session/fold-coordinator";
|
|
20
21
|
import type { CustomMessage } from "../session/messages";
|
|
21
22
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
22
23
|
import type { SkillActiveEntry } from "../skill-state/active-state";
|
|
@@ -235,12 +236,13 @@ export interface ToolSession {
|
|
|
235
236
|
assertEvalExecutionAllowed?: () => void;
|
|
236
237
|
/** Track tool-owned eval work so session disposal can await/abort it like direct session eval runs. */
|
|
237
238
|
trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
|
|
238
|
-
/** Register a
|
|
239
|
-
|
|
240
|
-
/** Whether a
|
|
239
|
+
/** Register a foldable foreground wait so the fold chord can move it into a background job. */
|
|
240
|
+
registerForegroundFoldParticipant?: (adapter: FoldAdapter) => () => void;
|
|
241
|
+
/** Whether a foreground wait is currently foldable into a background job. */
|
|
241
242
|
hasForegroundBashBackgroundRequestHandler?: () => boolean;
|
|
242
|
-
/** Request that the active
|
|
243
|
-
requestForegroundBashBackground?: () => boolean
|
|
243
|
+
/** Request that the active foreground wait fold into a background job, if supported. */
|
|
244
|
+
requestForegroundBashBackground?: () => Promise<boolean>;
|
|
245
|
+
|
|
244
246
|
/** Get the session-owned or inherited async job manager. */
|
|
245
247
|
getAsyncJobManager?: () => AsyncJobManager | undefined;
|
|
246
248
|
/** Resolves when the session queues user steering (or `signal` aborts) without consuming it; wait-style tools use this to end their observation early. */
|
|
@@ -2319,7 +2319,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2319
2319
|
"job": {
|
|
2320
2320
|
"name": "job",
|
|
2321
2321
|
"label": "Job",
|
|
2322
|
-
"description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI,
|
|
2322
|
+
"description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
|
|
2323
2323
|
"parameters": {
|
|
2324
2324
|
"type": "object",
|
|
2325
2325
|
"properties": {
|
|
@@ -208,7 +208,15 @@ function chainCacheKey(
|
|
|
208
208
|
preferred ?? null,
|
|
209
209
|
fallbacks,
|
|
210
210
|
ctx
|
|
211
|
-
? [
|
|
211
|
+
? [
|
|
212
|
+
ctx.provider,
|
|
213
|
+
ctx.modelId,
|
|
214
|
+
ctx.wireModelId ?? null,
|
|
215
|
+
ctx.api,
|
|
216
|
+
ctx.baseUrl ?? null,
|
|
217
|
+
ctx.webSearch ?? null,
|
|
218
|
+
ctx.ownerAuthOverride ?? false,
|
|
219
|
+
]
|
|
212
220
|
: null,
|
|
213
221
|
]);
|
|
214
222
|
}
|
|
@@ -278,7 +286,17 @@ function isAnthropicWire(api: string): boolean {
|
|
|
278
286
|
}
|
|
279
287
|
|
|
280
288
|
function isGoogleWire(api: string): boolean {
|
|
281
|
-
return
|
|
289
|
+
return (
|
|
290
|
+
api === "google-generative-ai" ||
|
|
291
|
+
api === "google-generative-language" ||
|
|
292
|
+
api === "gemini-wire" ||
|
|
293
|
+
api === "google-vertex" ||
|
|
294
|
+
api === "google-gemini-cli"
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isGenerativeLanguageWire(api: string): boolean {
|
|
299
|
+
return api === "google-generative-ai" || api === "google-generative-language" || api === "gemini-wire";
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
function isOpenAICompatWire(api: string): boolean {
|
|
@@ -305,6 +323,52 @@ function looksXaiFamilyModelId(ctx: ActiveSearchModelContext): boolean {
|
|
|
305
323
|
return looksXaiModelId(ctx.wireModelId) || looksXaiModelId(ctx.modelId);
|
|
306
324
|
}
|
|
307
325
|
|
|
326
|
+
const OFFICIAL_GEMINI_HOSTS = new Set([
|
|
327
|
+
"cloudcode-pa.googleapis.com",
|
|
328
|
+
"daily-cloudcode-pa.googleapis.com",
|
|
329
|
+
"daily-cloudcode-pa.sandbox.googleapis.com",
|
|
330
|
+
"generativelanguage.googleapis.com",
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
function normalizedHost(baseUrl: string): string | undefined {
|
|
334
|
+
try {
|
|
335
|
+
return new URL(baseUrl).hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
|
|
336
|
+
} catch {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isOfficialGeminiBaseUrl(baseUrl: string | undefined): boolean {
|
|
342
|
+
if (!baseUrl?.trim()) return true;
|
|
343
|
+
const host = normalizedHost(baseUrl);
|
|
344
|
+
return host !== undefined && OFFICIAL_GEMINI_HOSTS.has(host);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isOfficialXaiBaseUrl(baseUrl: string | undefined): boolean {
|
|
348
|
+
if (!baseUrl?.trim()) return true;
|
|
349
|
+
return normalizedHost(baseUrl) === "api.x.ai";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const CANONICAL_GEMINI_PROVIDERS = new Set(["google", "gemini", "google-gemini-cli", "google-antigravity"]);
|
|
353
|
+
|
|
354
|
+
function hasCustomGeminiSearchContext(ctx: ActiveSearchModelContext): boolean {
|
|
355
|
+
if (!isGoogleWire(ctx.api)) return false;
|
|
356
|
+
if (ctx.ownerAuthOverride) return true;
|
|
357
|
+
if (!isOfficialGeminiBaseUrl(ctx.baseUrl)) return true;
|
|
358
|
+
const provider = ctx.provider.toLowerCase();
|
|
359
|
+
return Boolean(ctx.resolveCredentials && !CANONICAL_GEMINI_PROVIDERS.has(provider));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasCustomXaiSearchContext(ctx: ActiveSearchModelContext): boolean {
|
|
363
|
+
if (!isOpenAICompatWire(ctx.api)) return false;
|
|
364
|
+
if (ctx.ownerAuthOverride) return true;
|
|
365
|
+
const provider = ctx.provider.toLowerCase();
|
|
366
|
+
if (provider === "xai" && !isOfficialXaiBaseUrl(ctx.baseUrl)) return true;
|
|
367
|
+
if (!looksXaiFamilyModelId(ctx)) return false;
|
|
368
|
+
if (!isOfficialXaiBaseUrl(ctx.baseUrl)) return true;
|
|
369
|
+
return provider !== "xai";
|
|
370
|
+
}
|
|
371
|
+
|
|
308
372
|
export function isLocalBaseUrl(baseUrl: string | undefined): boolean {
|
|
309
373
|
if (!baseUrl) return false;
|
|
310
374
|
let url: URL;
|
|
@@ -369,12 +433,29 @@ export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | und
|
|
|
369
433
|
if (!ctx || ctx.webSearch === "off") return undefined;
|
|
370
434
|
const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
|
|
371
435
|
if (modelId.startsWith("claude-") && isAnthropicWire(ctx.api)) return "anthropic";
|
|
372
|
-
if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api)) return "gemini";
|
|
373
|
-
|
|
436
|
+
if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api) && !hasCustomGeminiSearchContext(ctx)) return "gemini";
|
|
437
|
+
// A custom proxy may expose a Grok model through an OpenAI wire while the
|
|
438
|
+
// process also has canonical xAI credentials. Do not infer the canonical
|
|
439
|
+
// xAI provider from the model family: that would silently send the search to
|
|
440
|
+
// api.x.ai with another registry's credentials. Custom active contexts are
|
|
441
|
+
// dispatched below by activeContextNativeId, where the provider can consume
|
|
442
|
+
// the active owner-bound resolver instead.
|
|
443
|
+
if (
|
|
444
|
+
looksXaiFamilyModelId(ctx) &&
|
|
445
|
+
isOpenAICompatWire(ctx.api) &&
|
|
446
|
+
ctx.provider.toLowerCase() === "xai" &&
|
|
447
|
+
!hasCustomXaiSearchContext(ctx)
|
|
448
|
+
)
|
|
449
|
+
return "xai";
|
|
374
450
|
// `codex` hits the ChatGPT backend with local Codex OAuth, so only infer it
|
|
375
451
|
// for genuine OpenAI endpoints. Custom/proxy OpenAI-compatible models fall
|
|
376
|
-
// through to `activeContextNativeId`
|
|
377
|
-
if (
|
|
452
|
+
// through to `activeContextNativeId` and reuse their own credentials.
|
|
453
|
+
if (
|
|
454
|
+
!ctx.ownerAuthOverride &&
|
|
455
|
+
looksOpenAIFamilyModelId(ctx) &&
|
|
456
|
+
isOpenAICompatWire(ctx.api) &&
|
|
457
|
+
isOpenAIOfficialBaseUrl(ctx.baseUrl)
|
|
458
|
+
) {
|
|
378
459
|
return "codex";
|
|
379
460
|
}
|
|
380
461
|
return undefined;
|
|
@@ -382,7 +463,10 @@ export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | und
|
|
|
382
463
|
|
|
383
464
|
function canUseDirectProviderMapping(ctx: ActiveSearchModelContext, id: SearchProviderId): boolean {
|
|
384
465
|
if (ctx.webSearch === "off") return false;
|
|
466
|
+
if (id === "gemini" && hasCustomGeminiSearchContext(ctx)) return false;
|
|
467
|
+
if (id === "xai" && hasCustomXaiSearchContext(ctx)) return false;
|
|
385
468
|
if (id !== "codex") return true;
|
|
469
|
+
if (ctx.ownerAuthOverride) return false;
|
|
386
470
|
// Same constraint as inference: the ChatGPT-backed codex provider is valid
|
|
387
471
|
// only for official OpenAI endpoints, not custom/proxy base URLs.
|
|
388
472
|
return isOpenAIOfficialBaseUrl(ctx.baseUrl);
|
|
@@ -395,6 +479,10 @@ export async function canUseGenericCredentials(
|
|
|
395
479
|
signal?: AbortSignal,
|
|
396
480
|
): Promise<boolean> {
|
|
397
481
|
if (!ctx) return false;
|
|
482
|
+
if (ctx.resolveCredentials) {
|
|
483
|
+
const resolved = await ctx.resolveCredentials({ sessionId, signal });
|
|
484
|
+
return Boolean(resolved.apiKey) || hasActiveCredentialHeader(resolved.headers);
|
|
485
|
+
}
|
|
398
486
|
const key = await authStorage.getApiKey(ctx.provider, sessionId, {
|
|
399
487
|
baseUrl: ctx.baseUrl,
|
|
400
488
|
modelId: ctx.modelId,
|
|
@@ -403,6 +491,13 @@ export async function canUseGenericCredentials(
|
|
|
403
491
|
return Boolean(key);
|
|
404
492
|
}
|
|
405
493
|
|
|
494
|
+
function hasActiveCredentialHeader(headers: Record<string, string> | undefined): boolean {
|
|
495
|
+
return Object.keys(headers ?? {}).some(key => {
|
|
496
|
+
const normalized = key.toLowerCase();
|
|
497
|
+
return normalized === "authorization" || normalized === "x-api-key" || normalized === "x-goog-api-key";
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
406
501
|
/**
|
|
407
502
|
* Native web-search provider to attempt by reusing the ACTIVE model's own
|
|
408
503
|
* credentials + baseUrl, dispatched by the model's wire protocol.
|
|
@@ -424,13 +519,20 @@ export function activeContextNativeId(ctx: ActiveSearchModelContext | undefined)
|
|
|
424
519
|
if (!ctx || ctx.webSearch === "off") return undefined;
|
|
425
520
|
const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
|
|
426
521
|
// Dispatch must match exactly what each provider can service by reusing the
|
|
427
|
-
// active credential:
|
|
428
|
-
// OpenAI
|
|
429
|
-
//
|
|
430
|
-
//
|
|
522
|
+
// active credential: xAI-family models use the xAI search tools, the
|
|
523
|
+
// OpenAI-compatible adapter only speaks the two plain OpenAI wires (not
|
|
524
|
+
// azure), and the Gemini active path only speaks the public Generative
|
|
525
|
+
// Language wire (not vertex/cloud-code). Returning an id the provider would
|
|
526
|
+
// reject just wastes a guaranteed-fail attempt before DuckDuckGo.
|
|
431
527
|
if (isAnthropicWire(ctx.api) && modelId.startsWith("claude-")) return "anthropic";
|
|
528
|
+
if (isGenerativeLanguageWire(ctx.api) && modelId.startsWith("gemini-")) return "gemini";
|
|
529
|
+
if (
|
|
530
|
+
isOpenAICompatWire(ctx.api) &&
|
|
531
|
+
hasCustomXaiSearchContext(ctx) &&
|
|
532
|
+
(looksXaiFamilyModelId(ctx) || ctx.provider.toLowerCase() === "xai")
|
|
533
|
+
)
|
|
534
|
+
return "xai";
|
|
432
535
|
if (ctx.api === "openai-responses" || ctx.api === "openai-completions") return "openai-compatible";
|
|
433
|
-
if (ctx.api === "google-generative-ai" && modelId.startsWith("gemini-")) return "gemini";
|
|
434
536
|
return undefined;
|
|
435
537
|
}
|
|
436
538
|
|
|
@@ -445,6 +547,13 @@ export async function resolveProviderChain(options: ResolveProviderChainOptions)
|
|
|
445
547
|
settings,
|
|
446
548
|
} = options;
|
|
447
549
|
|
|
550
|
+
// Resolve the active model before consulting the chain cache. The registry
|
|
551
|
+
// resolver refreshes rotating apiKeyEnv credentials and updates generated
|
|
552
|
+
// headers, which also advances AuthStorage's generation when credentials
|
|
553
|
+
// rotate or disappear.
|
|
554
|
+
if (preferredProvider === "auto" && activeModelContext?.resolveCredentials) {
|
|
555
|
+
await activeModelContext.resolveCredentials({ sessionId, signal });
|
|
556
|
+
}
|
|
448
557
|
const cacheKey = chainCacheKey(preferredProvider, fallbackProviders, activeModelContext, authStorage, settings);
|
|
449
558
|
const perStorage = chainCache.get(authStorage);
|
|
450
559
|
const now = Date.now();
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "@gajae-code/ai/utils/anthropic-auth";
|
|
19
19
|
import { $credentialEnv, $env } from "@gajae-code/utils";
|
|
20
20
|
import type {
|
|
21
|
+
ActiveSearchModelCredentials,
|
|
21
22
|
AnthropicApiResponse,
|
|
22
23
|
AnthropicCitation,
|
|
23
24
|
SearchCitation,
|
|
@@ -73,6 +74,28 @@ function buildSystemBlocks(
|
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function mergeHeadersPreservingExplicit(
|
|
78
|
+
baseHeaders: Record<string, string>,
|
|
79
|
+
explicitHeaders: Record<string, string> | undefined,
|
|
80
|
+
): Record<string, string> {
|
|
81
|
+
const merged = { ...baseHeaders };
|
|
82
|
+
for (const [key, value] of Object.entries(explicitHeaders ?? {})) {
|
|
83
|
+
for (const existingKey of Object.keys(merged)) {
|
|
84
|
+
if (existingKey.toLowerCase() === key.toLowerCase()) delete merged[existingKey];
|
|
85
|
+
}
|
|
86
|
+
merged[key] = value;
|
|
87
|
+
}
|
|
88
|
+
return merged;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function explicitCredentialHeader(headers: Record<string, string> | undefined): string | undefined {
|
|
92
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
93
|
+
const normalized = key.toLowerCase();
|
|
94
|
+
if (normalized === "authorization" || normalized === "x-api-key") return value;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
/**
|
|
77
100
|
* Calls the Anthropic API with web search tool enabled.
|
|
78
101
|
* @param auth - Authentication configuration (API key or OAuth)
|
|
@@ -93,7 +116,7 @@ async function callSearch(
|
|
|
93
116
|
extraHeaders?: Record<string, string>,
|
|
94
117
|
): Promise<AnthropicApiResponse> {
|
|
95
118
|
const url = buildAnthropicUrl(auth);
|
|
96
|
-
const headers =
|
|
119
|
+
const headers = mergeHeadersPreservingExplicit(buildAnthropicSearchHeaders(auth), extraHeaders);
|
|
97
120
|
|
|
98
121
|
const systemBlocks = buildSystemBlocks(auth, model, systemPrompt);
|
|
99
122
|
|
|
@@ -284,29 +307,78 @@ export async function searchAnthropic(
|
|
|
284
307
|
// proxy), prefer its wire model id and carry its request headers through.
|
|
285
308
|
let modelOverride: string | undefined;
|
|
286
309
|
let extraHeaders: Record<string, string> | undefined;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
const activeContext = "authStorage" in params ? params.activeModelContext : undefined;
|
|
311
|
+
const activeSessionId = "authStorage" in params ? params.sessionId : undefined;
|
|
312
|
+
const activeCustomContext =
|
|
313
|
+
activeContext?.api === "anthropic-messages" &&
|
|
314
|
+
(activeContext.ownerAuthOverride || activeContext.provider.toLowerCase() !== "anthropic");
|
|
315
|
+
|
|
316
|
+
if (activeCustomContext && activeContext?.resolveCredentials) {
|
|
317
|
+
const activeCredentials = await activeContext.resolveCredentials({
|
|
318
|
+
sessionId: activeSessionId,
|
|
292
319
|
signal: params.signal,
|
|
293
320
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
321
|
+
const ctxKey =
|
|
322
|
+
activeCredentials.apiKey ?? explicitCredentialHeader(activeCredentials.headers ?? activeContext.headers);
|
|
323
|
+
if (ctxKey) {
|
|
324
|
+
auth = buildAnthropicAuthConfig(ctxKey, activeContext.baseUrl);
|
|
325
|
+
modelOverride = activeContext.wireModelId ?? activeContext.modelId;
|
|
326
|
+
extraHeaders = activeCredentials.headers ?? activeContext.headers;
|
|
327
|
+
}
|
|
328
|
+
} else if (searchApiKey) {
|
|
329
|
+
auth = buildAnthropicAuthConfig(searchApiKey, searchBaseUrl);
|
|
330
|
+
} else if ("authStorage" in params) {
|
|
331
|
+
const ctx = activeContext;
|
|
332
|
+
const resolveActiveCredentials = ctx?.resolveCredentials;
|
|
333
|
+
const activeAnthropicWire = ctx?.api === "anthropic-messages";
|
|
334
|
+
const activeModelOwnsProvider =
|
|
335
|
+
ctx !== undefined &&
|
|
336
|
+
resolveActiveCredentials !== undefined &&
|
|
337
|
+
activeAnthropicWire &&
|
|
338
|
+
ctx.provider.toLowerCase() === "anthropic";
|
|
339
|
+
const activeCustomModel =
|
|
340
|
+
ctx !== undefined &&
|
|
341
|
+
resolveActiveCredentials !== undefined &&
|
|
342
|
+
activeAnthropicWire &&
|
|
343
|
+
ctx.provider.toLowerCase() !== "anthropic";
|
|
344
|
+
if (activeModelOwnsProvider || activeCustomModel) {
|
|
345
|
+
const activeCredentials: ActiveSearchModelCredentials = await resolveActiveCredentials({
|
|
346
|
+
sessionId: params.sessionId,
|
|
304
347
|
signal: params.signal,
|
|
305
348
|
});
|
|
349
|
+
const ctxKey = activeCredentials.apiKey ?? explicitCredentialHeader(activeCredentials.headers ?? ctx.headers);
|
|
306
350
|
if (ctxKey) {
|
|
307
351
|
auth = buildAnthropicAuthConfig(ctxKey, ctx.baseUrl);
|
|
308
352
|
modelOverride = ctx.wireModelId ?? ctx.modelId;
|
|
309
|
-
extraHeaders = ctx.headers;
|
|
353
|
+
extraHeaders = activeCredentials.headers ?? ctx.headers;
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
const apiKey = await params.authStorage.getApiKey("anthropic", params.sessionId, {
|
|
357
|
+
signal: params.signal,
|
|
358
|
+
});
|
|
359
|
+
if (apiKey) auth = buildAnthropicAuthConfig(apiKey);
|
|
360
|
+
|
|
361
|
+
// Fall back to the active model's own credentials + baseUrl when no
|
|
362
|
+
// canonical Anthropic key exists but the active model speaks the
|
|
363
|
+
// Anthropic wire (e.g. Claude served through a proxy).
|
|
364
|
+
if (!auth && ctx && ctx.api === "anthropic-messages") {
|
|
365
|
+
const activeCredentials: ActiveSearchModelCredentials = ctx.resolveCredentials
|
|
366
|
+
? await ctx.resolveCredentials({ sessionId: params.sessionId, signal: params.signal })
|
|
367
|
+
: {
|
|
368
|
+
apiKey: await params.authStorage.getApiKey(ctx.provider, params.sessionId, {
|
|
369
|
+
baseUrl: ctx.baseUrl,
|
|
370
|
+
modelId: ctx.modelId,
|
|
371
|
+
signal: params.signal,
|
|
372
|
+
}),
|
|
373
|
+
headers: ctx.headers,
|
|
374
|
+
};
|
|
375
|
+
const ctxKey =
|
|
376
|
+
activeCredentials.apiKey ?? explicitCredentialHeader(activeCredentials.headers ?? ctx.headers);
|
|
377
|
+
if (ctxKey) {
|
|
378
|
+
auth = buildAnthropicAuthConfig(ctxKey, ctx.baseUrl);
|
|
379
|
+
modelOverride = ctx.wireModelId ?? ctx.modelId;
|
|
380
|
+
extraHeaders = activeCredentials.headers ?? ctx.headers;
|
|
381
|
+
}
|
|
310
382
|
}
|
|
311
383
|
}
|
|
312
384
|
}
|
|
@@ -20,7 +20,10 @@ export const SEARXNG_BEARER_CREDENTIAL_PREFIX = "gjc-searxng-bearer:";
|
|
|
20
20
|
/**
|
|
21
21
|
* Shared web search parameters passed to providers.
|
|
22
22
|
*
|
|
23
|
-
* `authStorage` is the
|
|
23
|
+
* `authStorage` is the credential source for canonical providers. Native
|
|
24
|
+
* active-model search may instead use `activeModelContext.resolveCredentials`,
|
|
25
|
+
* which is supplied by ModelRegistry so rotating config credentials and
|
|
26
|
+
* generated headers are refreshed before the request.
|
|
24
27
|
* Opening a sibling SQLite handle or calling provider-direct refresh helpers
|
|
25
28
|
* (e.g. `refreshOpenAIOpenAI code backendToken`, `refreshGoogleCloudToken`) is prohibited:
|
|
26
29
|
* it races the broker's per-credential refresh and POSTs the broker sentinel
|
|
@@ -75,8 +78,8 @@ export interface SearchParams {
|
|
|
75
78
|
codeExecution?: Record<string, unknown>;
|
|
76
79
|
urlContext?: Record<string, unknown>;
|
|
77
80
|
/**
|
|
78
|
-
* The
|
|
79
|
-
* handle
|
|
81
|
+
* The canonical source of truth for credentials. Providers MUST consult this
|
|
82
|
+
* handle (`getApiKey` for bearer-style auth, `getOAuthAccess`
|
|
80
83
|
* when identity metadata is required). Do not open `AgentStorage` or any
|
|
81
84
|
* `AuthCredentialStore` directly — that bypasses the broker pipeline and
|
|
82
85
|
* the per-credential single-flight refresh.
|
|
@@ -108,7 +111,9 @@ export abstract class SearchProvider {
|
|
|
108
111
|
abstract isAvailable(authStorage: AuthStorage, settings?: SearchProviderSettings): Promise<boolean> | boolean;
|
|
109
112
|
|
|
110
113
|
/**
|
|
111
|
-
* Execute a search.
|
|
114
|
+
* Execute a search. Canonical credentials MUST be resolved through
|
|
115
|
+
* `params.authStorage`; active-model credentials MUST use the injected
|
|
116
|
+
* resolver when one is present.
|
|
112
117
|
*/
|
|
113
118
|
abstract search(params: SearchParams): Promise<SearchResponse>;
|
|
114
119
|
}
|
|
@@ -16,7 +16,13 @@ import {
|
|
|
16
16
|
} from "@gajae-code/ai/providers/google-gemini-headers";
|
|
17
17
|
import { fetchWithRetry } from "@gajae-code/utils";
|
|
18
18
|
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
ActiveSearchModelContext,
|
|
21
|
+
ActiveSearchModelCredentials,
|
|
22
|
+
SearchCitation,
|
|
23
|
+
SearchResponse,
|
|
24
|
+
SearchSource,
|
|
25
|
+
} from "../../../web/search/types";
|
|
20
26
|
import { SearchProviderError } from "../../../web/search/types";
|
|
21
27
|
import type { SearchParams } from "./base";
|
|
22
28
|
import { SearchProvider } from "./base";
|
|
@@ -38,6 +44,49 @@ const RATE_LIMIT_BUDGET_MS = 30 * 1000;
|
|
|
38
44
|
const GEMINI_PROVIDERS = ["google-gemini-cli", "google-antigravity"] as const;
|
|
39
45
|
type GeminiProviderId = (typeof GEMINI_PROVIDERS)[number];
|
|
40
46
|
|
|
47
|
+
function isGenerativeLanguageWire(api: string): boolean {
|
|
48
|
+
return api === "google-generative-ai" || api === "google-generative-language" || api === "gemini-wire";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isGoogleWire(api: string): boolean {
|
|
52
|
+
return isGenerativeLanguageWire(api) || api === "google-gemini-cli" || api === "google-vertex";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const OFFICIAL_GEMINI_HOSTS = new Set([
|
|
56
|
+
"cloudcode-pa.googleapis.com",
|
|
57
|
+
"daily-cloudcode-pa.googleapis.com",
|
|
58
|
+
"daily-cloudcode-pa.sandbox.googleapis.com",
|
|
59
|
+
"generativelanguage.googleapis.com",
|
|
60
|
+
]);
|
|
61
|
+
const CANONICAL_GEMINI_PROVIDERS = new Set(["google", "gemini", "google-gemini-cli", "google-antigravity"]);
|
|
62
|
+
|
|
63
|
+
function isOfficialGeminiBaseUrl(baseUrl: string | undefined): boolean {
|
|
64
|
+
if (!baseUrl?.trim()) return true;
|
|
65
|
+
try {
|
|
66
|
+
const host = new URL(baseUrl).hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
|
|
67
|
+
return OFFICIAL_GEMINI_HOSTS.has(host);
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isCustomGeminiContext(ctx: ActiveSearchModelContext | undefined): boolean {
|
|
74
|
+
if (!ctx || !isGoogleWire(ctx.api)) return false;
|
|
75
|
+
if (ctx.ownerAuthOverride) return true;
|
|
76
|
+
if (!isOfficialGeminiBaseUrl(ctx.baseUrl)) return true;
|
|
77
|
+
const provider = ctx.provider.toLowerCase();
|
|
78
|
+
return Boolean(ctx.resolveCredentials && !CANONICAL_GEMINI_PROVIDERS.has(provider));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isCustomGenerativeLanguageContext(ctx: ActiveSearchModelContext | undefined): boolean {
|
|
82
|
+
return isGenerativeLanguageWire(ctx?.api ?? "") && isCustomGeminiContext(ctx);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
86
|
+
const normalized = name.toLowerCase();
|
|
87
|
+
return Object.keys(headers).some(key => key.toLowerCase() === normalized);
|
|
88
|
+
}
|
|
89
|
+
|
|
41
90
|
interface GeminiToolParams {
|
|
42
91
|
google_search?: Record<string, unknown>;
|
|
43
92
|
code_execution?: Record<string, unknown>;
|
|
@@ -433,19 +482,28 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
|
|
|
433
482
|
* Native Gemini web search over the public Generative Language REST API
|
|
434
483
|
* (`{baseUrl}/v1beta/models/{model}:generateContent`), reusing the ACTIVE
|
|
435
484
|
* model's own API key + baseUrl. This is the "native search over a proxy" path
|
|
436
|
-
* for
|
|
437
|
-
*
|
|
438
|
-
* Code Assist API with OAuth.
|
|
485
|
+
* for Generative Language wire models. Distinct from {@link searchGemini},
|
|
486
|
+
* which speaks the Cloud Code Assist API with OAuth.
|
|
439
487
|
*/
|
|
440
488
|
async function searchGeminiViaGenerativeLanguage(params: SearchParams): Promise<SearchResponse> {
|
|
441
489
|
const ctx = params.activeModelContext;
|
|
442
490
|
if (!ctx) throw new SearchProviderError("gemini", "Gemini web search requires active model context", 400);
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
491
|
+
const activeCredentials: ActiveSearchModelCredentials = ctx.resolveCredentials
|
|
492
|
+
? await ctx.resolveCredentials({ sessionId: params.sessionId, signal: params.signal })
|
|
493
|
+
: {
|
|
494
|
+
apiKey: await params.authStorage.getApiKey(ctx.provider, params.sessionId, {
|
|
495
|
+
baseUrl: ctx.baseUrl,
|
|
496
|
+
modelId: ctx.modelId,
|
|
497
|
+
signal: params.signal,
|
|
498
|
+
}),
|
|
499
|
+
headers: ctx.headers,
|
|
500
|
+
};
|
|
501
|
+
const apiKey = activeCredentials.apiKey;
|
|
502
|
+
const headers = { ...(activeCredentials.headers ?? ctx.headers ?? {}) };
|
|
503
|
+
if (apiKey && !hasHeader(headers, "x-goog-api-key")) headers["x-goog-api-key"] = apiKey;
|
|
504
|
+
if (!apiKey && !hasHeader(headers, "x-goog-api-key")) {
|
|
505
|
+
throw new SearchProviderError("gemini", `No credentials for ${ctx.provider}`, 401);
|
|
506
|
+
}
|
|
449
507
|
|
|
450
508
|
const model = ctx.wireModelId ?? ctx.modelId;
|
|
451
509
|
const base = (ctx.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
|
|
@@ -468,7 +526,10 @@ async function searchGeminiViaGenerativeLanguage(params: SearchParams): Promise<
|
|
|
468
526
|
|
|
469
527
|
const response = await fetch(url, {
|
|
470
528
|
method: "POST",
|
|
471
|
-
headers: {
|
|
529
|
+
headers: {
|
|
530
|
+
...headers,
|
|
531
|
+
...(hasHeader(headers, "content-type") ? {} : { "Content-Type": "application/json" }),
|
|
532
|
+
},
|
|
472
533
|
body: JSON.stringify(body),
|
|
473
534
|
signal: withHardTimeout(params.signal, "llm"),
|
|
474
535
|
});
|
|
@@ -534,11 +595,22 @@ export class GeminiProvider extends SearchProvider {
|
|
|
534
595
|
}
|
|
535
596
|
|
|
536
597
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
537
|
-
// Native-over-proxy: when canonical gemini-cli/antigravity OAuth is
|
|
538
|
-
// absent but the active model speaks the Generative Language wire, reuse
|
|
539
|
-
// its own API key + baseUrl instead of failing closed.
|
|
540
598
|
const ctx = params.activeModelContext;
|
|
541
|
-
|
|
599
|
+
// Native-over-proxy: a custom model's owner-bound resolver must win over
|
|
600
|
+
// canonical Gemini OAuth. Otherwise the request would silently leave the
|
|
601
|
+
// proxy and hit Cloud Code with another registry's identity.
|
|
602
|
+
if (isCustomGeminiContext(ctx)) {
|
|
603
|
+
if (isCustomGenerativeLanguageContext(ctx)) return searchGeminiViaGenerativeLanguage(params);
|
|
604
|
+
return Promise.reject(
|
|
605
|
+
new SearchProviderError(
|
|
606
|
+
"gemini",
|
|
607
|
+
`Gemini native search does not support custom ${ctx?.api ?? ""} transports`,
|
|
608
|
+
400,
|
|
609
|
+
),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
// Preserve the existing active-key fallback when canonical OAuth is absent.
|
|
613
|
+
if (!hasGeminiOAuth(params.authStorage) && ctx && isGenerativeLanguageWire(ctx.api)) {
|
|
542
614
|
return searchGeminiViaGenerativeLanguage(params);
|
|
543
615
|
}
|
|
544
616
|
return searchGemini({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SearchCitation, SearchResponse, SearchSource } from "../types";
|
|
1
|
+
import type { ActiveSearchModelCredentials, SearchCitation, SearchResponse, SearchSource } from "../types";
|
|
2
2
|
import { SearchProviderError } from "../types";
|
|
3
3
|
import type { SearchParams } from "./base";
|
|
4
4
|
import { SearchProvider } from "./base";
|
|
@@ -7,6 +7,11 @@ import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
|
7
7
|
|
|
8
8
|
type JsonObject = Record<string, unknown>;
|
|
9
9
|
|
|
10
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
11
|
+
const normalized = name.toLowerCase();
|
|
12
|
+
return Object.keys(headers).some(key => key.toLowerCase() === normalized);
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
function isJsonObject(value: unknown): value is JsonObject {
|
|
11
16
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
17
|
}
|
|
@@ -154,15 +159,25 @@ export class OpenAICompatibleSearchProvider extends SearchProvider {
|
|
|
154
159
|
if (ctx.api !== "openai-responses" && ctx.api !== "openai-completions") {
|
|
155
160
|
throw new SearchProviderError(this.id, `OpenAI-compatible web search does not support ${ctx.api}`, 400);
|
|
156
161
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
const activeCredentials: ActiveSearchModelCredentials = ctx.resolveCredentials
|
|
163
|
+
? await ctx.resolveCredentials({ sessionId: params.sessionId, signal: params.signal })
|
|
164
|
+
: {
|
|
165
|
+
apiKey: await params.authStorage.getApiKey(ctx.provider, params.sessionId, {
|
|
166
|
+
baseUrl: ctx.baseUrl,
|
|
167
|
+
modelId: ctx.modelId,
|
|
168
|
+
signal: params.signal,
|
|
169
|
+
}),
|
|
170
|
+
headers: ctx.headers,
|
|
171
|
+
};
|
|
172
|
+
const apiKey = activeCredentials.apiKey;
|
|
173
|
+
const headers = { ...(activeCredentials.headers ?? ctx.headers ?? {}) };
|
|
174
|
+
if (apiKey && !hasHeader(headers, "authorization")) headers.Authorization = `Bearer ${apiKey}`;
|
|
175
|
+
if (!hasHeader(headers, "authorization")) {
|
|
176
|
+
throw new SearchProviderError(this.id, `No credentials for ${ctx.provider}`, 401);
|
|
177
|
+
}
|
|
178
|
+
if (!hasHeader(headers, "content-type")) headers["Content-Type"] = "application/json";
|
|
163
179
|
const model = ctx.wireModelId ?? ctx.modelId;
|
|
164
180
|
const baseUrl = ctx.baseUrl ?? "";
|
|
165
|
-
const headers = { ...(ctx.headers ?? {}), Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
|
|
166
181
|
const messages = [
|
|
167
182
|
{ role: "system", content: params.systemPrompt },
|
|
168
183
|
{ role: "user", content: params.query },
|