@gajae-code/coding-agent 0.5.0 → 0.5.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 +36 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +29 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +27 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +5 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli/setup-cli.ts +138 -3
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +193 -0
- package/src/config/file-lock.ts +66 -10
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +39 -30
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +232 -19
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +48 -30
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +177 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +14 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +22 -12
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +18 -3
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +51 -8
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +81 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +266 -34
- package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +29 -4
- package/src/session/agent-session.ts +99 -19
- package/src/session/blob-store.ts +59 -3
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +72 -20
- package/src/setup/credential-import.ts +429 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +16 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +59 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +128 -29
- package/src/tools/subagent.ts +173 -9
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AuthStorage } from "@gajae-code/ai";
|
|
12
12
|
import type { SearchProvider } from "./providers/base";
|
|
13
|
-
import type { SearchProviderId } from "./types";
|
|
13
|
+
import type { ActiveSearchModelContext, SearchProviderId } from "./types";
|
|
14
|
+
import { isConfigurableSearchProviderId } from "./types";
|
|
14
15
|
|
|
15
16
|
export type { SearchParams } from "./providers/base";
|
|
16
17
|
export { SearchProvider } from "./providers/base";
|
|
@@ -23,36 +24,16 @@ interface ProviderMeta {
|
|
|
23
24
|
|
|
24
25
|
/** Lazy factories. Each `load()` dynamic-imports its provider module on first call. */
|
|
25
26
|
const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
26
|
-
exa: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
load: async () => new (await import("./providers/exa")).ExaProvider(),
|
|
30
|
-
},
|
|
31
|
-
brave: {
|
|
32
|
-
id: "brave",
|
|
33
|
-
label: "Brave",
|
|
34
|
-
load: async () => new (await import("./providers/brave")).BraveProvider(),
|
|
35
|
-
},
|
|
36
|
-
jina: {
|
|
37
|
-
id: "jina",
|
|
38
|
-
label: "Jina",
|
|
39
|
-
load: async () => new (await import("./providers/jina")).JinaProvider(),
|
|
40
|
-
},
|
|
27
|
+
exa: { id: "exa", label: "Exa", load: async () => new (await import("./providers/exa")).ExaProvider() },
|
|
28
|
+
brave: { id: "brave", label: "Brave", load: async () => new (await import("./providers/brave")).BraveProvider() },
|
|
29
|
+
jina: { id: "jina", label: "Jina", load: async () => new (await import("./providers/jina")).JinaProvider() },
|
|
41
30
|
perplexity: {
|
|
42
31
|
id: "perplexity",
|
|
43
32
|
label: "Perplexity",
|
|
44
33
|
load: async () => new (await import("./providers/perplexity")).PerplexityProvider(),
|
|
45
34
|
},
|
|
46
|
-
kimi: {
|
|
47
|
-
|
|
48
|
-
label: "Kimi",
|
|
49
|
-
load: async () => new (await import("./providers/kimi")).KimiProvider(),
|
|
50
|
-
},
|
|
51
|
-
zai: {
|
|
52
|
-
id: "zai",
|
|
53
|
-
label: "Z.AI",
|
|
54
|
-
load: async () => new (await import("./providers/zai")).ZaiProvider(),
|
|
55
|
-
},
|
|
35
|
+
kimi: { id: "kimi", label: "Kimi", load: async () => new (await import("./providers/kimi")).KimiProvider() },
|
|
36
|
+
zai: { id: "zai", label: "Z.AI", load: async () => new (await import("./providers/zai")).ZaiProvider() },
|
|
56
37
|
anthropic: {
|
|
57
38
|
id: "anthropic",
|
|
58
39
|
label: "Anthropic",
|
|
@@ -63,11 +44,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
63
44
|
label: "Gemini",
|
|
64
45
|
load: async () => new (await import("./providers/gemini")).GeminiProvider(),
|
|
65
46
|
},
|
|
66
|
-
codex: {
|
|
67
|
-
id: "codex",
|
|
68
|
-
label: "OpenAI",
|
|
69
|
-
load: async () => new (await import("./providers/codex")).CodexProvider(),
|
|
70
|
-
},
|
|
47
|
+
codex: { id: "codex", label: "OpenAI", load: async () => new (await import("./providers/codex")).CodexProvider() },
|
|
71
48
|
tavily: {
|
|
72
49
|
id: "tavily",
|
|
73
50
|
label: "Tavily",
|
|
@@ -78,11 +55,7 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
78
55
|
label: "Parallel",
|
|
79
56
|
load: async () => new (await import("./providers/parallel")).ParallelProvider(),
|
|
80
57
|
},
|
|
81
|
-
kagi: {
|
|
82
|
-
id: "kagi",
|
|
83
|
-
label: "Kagi",
|
|
84
|
-
load: async () => new (await import("./providers/kagi")).KagiProvider(),
|
|
85
|
-
},
|
|
58
|
+
kagi: { id: "kagi", label: "Kagi", load: async () => new (await import("./providers/kagi")).KagiProvider() },
|
|
86
59
|
synthetic: {
|
|
87
60
|
id: "synthetic",
|
|
88
61
|
label: "Synthetic",
|
|
@@ -98,26 +71,24 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
|
98
71
|
label: "DuckDuckGo",
|
|
99
72
|
load: async () => new (await import("./providers/duckduckgo")).DuckDuckGoProvider(),
|
|
100
73
|
},
|
|
74
|
+
"openai-compatible": {
|
|
75
|
+
id: "openai-compatible",
|
|
76
|
+
label: "OpenAI-compatible",
|
|
77
|
+
load: async () => new (await import("./providers/openai-compatible")).OpenAICompatibleSearchProvider(),
|
|
78
|
+
},
|
|
101
79
|
};
|
|
102
80
|
|
|
103
81
|
const instanceCache = new Map<SearchProviderId, SearchProvider>();
|
|
104
82
|
|
|
105
|
-
/** Cheap, sync metadata accessor — never triggers a provider load. */
|
|
106
83
|
export function getSearchProviderLabel(id: SearchProviderId): string {
|
|
107
84
|
return PROVIDER_META[id]?.label ?? id;
|
|
108
85
|
}
|
|
109
86
|
|
|
110
|
-
/**
|
|
111
|
-
* Resolve and cache a provider instance. First call for a given id loads the
|
|
112
|
-
* underlying module; subsequent calls return the cached singleton.
|
|
113
|
-
*/
|
|
114
87
|
export async function getSearchProvider(id: SearchProviderId): Promise<SearchProvider> {
|
|
115
88
|
const cached = instanceCache.get(id);
|
|
116
89
|
if (cached) return cached;
|
|
117
90
|
const meta = PROVIDER_META[id];
|
|
118
|
-
if (!meta) {
|
|
119
|
-
throw new Error(`Unknown search provider: ${id}`);
|
|
120
|
-
}
|
|
91
|
+
if (!meta) throw new Error(`Unknown search provider: ${id}`);
|
|
121
92
|
const provider = await meta.load();
|
|
122
93
|
instanceCache.set(id, provider);
|
|
123
94
|
return provider;
|
|
@@ -141,13 +112,6 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
|
|
|
141
112
|
"searxng",
|
|
142
113
|
];
|
|
143
114
|
|
|
144
|
-
/**
|
|
145
|
-
* Map an active model's provider string to its own native web-search provider.
|
|
146
|
-
* Keys are real model provider ids (see packages/ai/src/types.ts KnownProvider);
|
|
147
|
-
* a few aliases (gemini/kimi) and API strings (openai-responses) are tolerated
|
|
148
|
-
* defensively. Providers absent from this map (custom/unknown) fall through to
|
|
149
|
-
* DuckDuckGo.
|
|
150
|
-
*/
|
|
151
115
|
const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
|
|
152
116
|
openai: "codex",
|
|
153
117
|
"openai-codex": "codex",
|
|
@@ -165,54 +129,181 @@ const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
|
|
|
165
129
|
synthetic: "synthetic",
|
|
166
130
|
};
|
|
167
131
|
|
|
168
|
-
/** Preferred provider set via settings (default: auto) */
|
|
169
132
|
let preferredProvId: SearchProviderId | "auto" = "auto";
|
|
133
|
+
let fallbackProvIds: SearchProviderId[] = [];
|
|
170
134
|
|
|
171
|
-
/** Set the preferred web search provider from settings */
|
|
172
135
|
export function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void {
|
|
173
136
|
preferredProvId = provider;
|
|
174
137
|
}
|
|
175
138
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
139
|
+
export function setSearchFallbackProviders(ids: readonly string[]): void {
|
|
140
|
+
fallbackProvIds = ids.filter(isConfigurableSearchProviderId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ResolveProviderChainOptions {
|
|
144
|
+
authStorage: AuthStorage;
|
|
145
|
+
sessionId?: string;
|
|
146
|
+
signal?: AbortSignal;
|
|
147
|
+
preferredProvider?: SearchProviderId | "auto";
|
|
148
|
+
activeModelContext?: ActiveSearchModelContext;
|
|
149
|
+
fallbackProviders?: readonly SearchProviderId[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function appendAvailable(
|
|
153
|
+
chain: SearchProviderId[],
|
|
154
|
+
id: SearchProviderId,
|
|
155
|
+
authStorage: AuthStorage,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
if (chain.includes(id)) return;
|
|
158
|
+
const provider = await getSearchProvider(id);
|
|
159
|
+
if (await provider.isAvailable(authStorage)) chain.push(id);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function appendDeduped(chain: SearchProviderId[], id: SearchProviderId): void {
|
|
163
|
+
if (!chain.includes(id)) chain.push(id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isAnthropicWire(api: string): boolean {
|
|
167
|
+
return api === "anthropic-messages";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isGoogleWire(api: string): boolean {
|
|
171
|
+
return api === "google-generative-ai" || api === "google-vertex" || api === "google-gemini-cli";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isOpenAICompatWire(api: string): boolean {
|
|
175
|
+
return api === "openai-responses" || api === "openai-completions" || api === "azure-openai-responses";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function looksHostedModelId(modelId: string | undefined): boolean {
|
|
179
|
+
if (!modelId) return false;
|
|
180
|
+
const id = modelId.toLowerCase();
|
|
181
|
+
return /^(gpt-|o\d|o-|chatgpt-|text-|davinci|babbage|curie)/.test(id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function looksOpenAIFamilyModelId(ctx: ActiveSearchModelContext): boolean {
|
|
185
|
+
return looksHostedModelId(ctx.wireModelId) || looksHostedModelId(ctx.modelId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function isLocalBaseUrl(baseUrl: string | undefined): boolean {
|
|
189
|
+
if (!baseUrl) return false;
|
|
190
|
+
let url: URL;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(baseUrl);
|
|
193
|
+
} catch {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const host = url.hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
|
|
197
|
+
if (
|
|
198
|
+
host === "localhost" ||
|
|
199
|
+
host.endsWith(".localhost") ||
|
|
200
|
+
host === "host.docker.internal" ||
|
|
201
|
+
host.endsWith(".local")
|
|
202
|
+
)
|
|
203
|
+
return true;
|
|
204
|
+
const v4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
205
|
+
if (v4) {
|
|
206
|
+
const [a, b] = v4.slice(1, 3).map(Number);
|
|
207
|
+
if (
|
|
208
|
+
a === 127 ||
|
|
209
|
+
a === 0 ||
|
|
210
|
+
a === 10 ||
|
|
211
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
212
|
+
(a === 192 && b === 168) ||
|
|
213
|
+
(a === 169 && b === 254)
|
|
214
|
+
)
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (host === "::1" || host === "::") return true;
|
|
218
|
+
if (host.startsWith("fc") || host.startsWith("fd")) return true;
|
|
219
|
+
if (host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb"))
|
|
220
|
+
return true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | undefined): SearchProviderId | undefined {
|
|
225
|
+
if (!ctx || ctx.webSearch === "off") return undefined;
|
|
226
|
+
const modelId = (ctx.wireModelId ?? ctx.modelId).toLowerCase();
|
|
227
|
+
if (modelId.startsWith("claude-") && isAnthropicWire(ctx.api)) return "anthropic";
|
|
228
|
+
if (modelId.startsWith("gemini-") && isGoogleWire(ctx.api)) return "gemini";
|
|
229
|
+
if (looksOpenAIFamilyModelId(ctx) && isOpenAICompatWire(ctx.api)) {
|
|
230
|
+
if (ctx.webSearch === "on" || !isLocalBaseUrl(ctx.baseUrl)) return "codex";
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function canUseDirectProviderMapping(ctx: ActiveSearchModelContext, id: SearchProviderId): boolean {
|
|
236
|
+
if (ctx.webSearch === "off") return false;
|
|
237
|
+
if (id !== "codex") return true;
|
|
238
|
+
if (!isOpenAICompatWire(ctx.api)) return true;
|
|
239
|
+
return ctx.webSearch === "on" || !isLocalBaseUrl(ctx.baseUrl);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function canUseGenericCredentials(
|
|
189
243
|
authStorage: AuthStorage,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
ctx: ActiveSearchModelContext | undefined,
|
|
245
|
+
sessionId?: string,
|
|
246
|
+
signal?: AbortSignal,
|
|
247
|
+
): Promise<boolean> {
|
|
248
|
+
if (!ctx) return false;
|
|
249
|
+
const key = await authStorage.getApiKey(ctx.provider, sessionId, {
|
|
250
|
+
baseUrl: ctx.baseUrl,
|
|
251
|
+
modelId: ctx.modelId,
|
|
252
|
+
signal,
|
|
253
|
+
});
|
|
254
|
+
return Boolean(key);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function shouldTryGenericOpenAICompat(
|
|
258
|
+
authStorage: AuthStorage,
|
|
259
|
+
ctx: ActiveSearchModelContext | undefined,
|
|
260
|
+
sessionId?: string,
|
|
261
|
+
signal?: AbortSignal,
|
|
262
|
+
): Promise<boolean> {
|
|
263
|
+
if (!ctx || ctx.webSearch === "off" || !isOpenAICompatWire(ctx.api)) return false;
|
|
264
|
+
const autoAllowed =
|
|
265
|
+
ctx.webSearch === "on" ||
|
|
266
|
+
((ctx.api === "openai-responses" || looksOpenAIFamilyModelId(ctx)) && !isLocalBaseUrl(ctx.baseUrl));
|
|
267
|
+
return autoAllowed && (await canUseGenericCredentials(authStorage, ctx, sessionId, signal));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function resolveProviderChain(options: ResolveProviderChainOptions): Promise<SearchProvider[]> {
|
|
271
|
+
const {
|
|
272
|
+
authStorage,
|
|
273
|
+
sessionId,
|
|
274
|
+
signal,
|
|
275
|
+
preferredProvider = preferredProvId,
|
|
276
|
+
activeModelContext,
|
|
277
|
+
fallbackProviders = fallbackProvIds,
|
|
278
|
+
} = options;
|
|
193
279
|
const chain: SearchProviderId[] = [];
|
|
194
280
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
281
|
+
// A forced primary is honored only when it is a user-configurable provider.
|
|
282
|
+
// The internal `openai-compatible` adapter (and any non-configurable value) is
|
|
283
|
+
// never selectable as a forced primary; such inputs fall through to auto
|
|
284
|
+
// native resolution instead of being injected into the chain.
|
|
285
|
+
if (preferredProvider !== "auto" && isConfigurableSearchProviderId(preferredProvider)) {
|
|
286
|
+
await appendAvailable(chain, preferredProvider, authStorage);
|
|
287
|
+
} else if (activeModelContext) {
|
|
288
|
+
const directId = MODEL_PROVIDER_TO_SEARCH[activeModelContext.provider.toLowerCase()];
|
|
289
|
+
if (directId && canUseDirectProviderMapping(activeModelContext, directId))
|
|
290
|
+
await appendAvailable(chain, directId, authStorage);
|
|
291
|
+
const inferred = inferNativeProviderFromModel(activeModelContext);
|
|
292
|
+
if (inferred) await appendAvailable(chain, inferred, authStorage);
|
|
293
|
+
if (await shouldTryGenericOpenAICompat(authStorage, activeModelContext, sessionId, signal))
|
|
294
|
+
appendDeduped(chain, "openai-compatible");
|
|
208
295
|
}
|
|
209
296
|
|
|
210
|
-
//
|
|
211
|
-
|
|
297
|
+
// Configured fallbacks are user-facing only: the internal `openai-compatible`
|
|
298
|
+
// adapter (and any non-configurable id) can never enter the chain through the
|
|
299
|
+
// fallback list, regardless of how `fallbackProviders` was supplied.
|
|
300
|
+
for (const id of fallbackProviders) {
|
|
301
|
+
if (!isConfigurableSearchProviderId(id)) continue;
|
|
302
|
+
await appendAvailable(chain, id, authStorage);
|
|
303
|
+
}
|
|
304
|
+
appendDeduped(chain, "duckduckgo");
|
|
212
305
|
|
|
213
306
|
const providers: SearchProvider[] = [];
|
|
214
|
-
for (const id of chain)
|
|
215
|
-
providers.push(await getSearchProvider(id));
|
|
216
|
-
}
|
|
307
|
+
for (const id of chain) providers.push(await getSearchProvider(id));
|
|
217
308
|
return providers;
|
|
218
309
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthStorage } from "@gajae-code/ai";
|
|
2
|
-
import type { SearchProviderId, SearchResponse } from "../types";
|
|
2
|
+
import type { ActiveSearchModelContext, SearchProviderId, SearchResponse } from "../types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Shared web search parameters passed to providers.
|
|
@@ -50,6 +50,7 @@ export interface SearchParams {
|
|
|
50
50
|
* caller's agent session when available; otherwise omit.
|
|
51
51
|
*/
|
|
52
52
|
sessionId?: string;
|
|
53
|
+
activeModelContext?: ActiveSearchModelContext;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Base class for web search providers. */
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { SearchCitation, SearchResponse, SearchSource } from "../types";
|
|
2
|
+
import { SearchProviderError } from "../types";
|
|
3
|
+
import type { SearchParams } from "./base";
|
|
4
|
+
import { SearchProvider } from "./base";
|
|
5
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
6
|
+
|
|
7
|
+
function endpoint(baseUrl: string, api: string): string {
|
|
8
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
9
|
+
return api === "openai-completions" ? `${base}/chat/completions` : `${base}/responses`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function textFromResponse(json: any): string | undefined {
|
|
13
|
+
if (typeof json.output_text === "string") return json.output_text;
|
|
14
|
+
const chunks: string[] = [];
|
|
15
|
+
for (const item of json.output ?? []) {
|
|
16
|
+
for (const content of item.content ?? []) {
|
|
17
|
+
if (typeof content.text === "string") chunks.push(content.text);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const chat = json.choices?.[0]?.message?.content;
|
|
21
|
+
if (typeof chat === "string") chunks.push(chat);
|
|
22
|
+
return chunks.join("\n") || undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pushCitation(out: SearchCitation[], rawUrl: unknown, rawTitle: unknown, rawText: unknown): void {
|
|
26
|
+
if (typeof rawUrl !== "string" || !rawUrl) return;
|
|
27
|
+
out.push({
|
|
28
|
+
url: rawUrl,
|
|
29
|
+
title: typeof rawTitle === "string" && rawTitle ? rawTitle : rawUrl,
|
|
30
|
+
citedText: typeof rawText === "string" ? rawText : undefined,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Only recognized grounding annotations count as citations. An OpenAI-compatible
|
|
35
|
+
// endpoint that ignores the web_search request returns a normal answer with no
|
|
36
|
+
// `url_citation` annotations; treating arbitrary URL/`type:"source"` metadata as a
|
|
37
|
+
// citation would mask that non-search answer as a real search result. Restrict
|
|
38
|
+
// extraction to the documented annotation shapes (Responses
|
|
39
|
+
// `output[].content[].annotations[]` and Chat `choices[].message.annotations[]`),
|
|
40
|
+
// accepting only `type: "url_citation"` entries.
|
|
41
|
+
function collectCitationAnnotations(annotations: unknown, out: SearchCitation[]): void {
|
|
42
|
+
if (!Array.isArray(annotations)) return;
|
|
43
|
+
for (const annotation of annotations) {
|
|
44
|
+
if (!annotation || typeof annotation !== "object") continue;
|
|
45
|
+
const ann = annotation as Record<string, any>;
|
|
46
|
+
if (ann.type !== "url_citation") continue;
|
|
47
|
+
const cite =
|
|
48
|
+
ann.url_citation && typeof ann.url_citation === "object" ? (ann.url_citation as Record<string, any>) : ann;
|
|
49
|
+
pushCitation(out, cite.url ?? cite.uri, cite.title, cite.text ?? cite.quote ?? ann.text);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseCitations(json: any): SearchCitation[] {
|
|
54
|
+
const citations: SearchCitation[] = [];
|
|
55
|
+
for (const item of json?.output ?? []) {
|
|
56
|
+
for (const content of item?.content ?? []) {
|
|
57
|
+
collectCitationAnnotations(content?.annotations, citations);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const choice of json?.choices ?? []) {
|
|
61
|
+
collectCitationAnnotations(choice?.message?.annotations, citations);
|
|
62
|
+
}
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
return citations.filter(c => {
|
|
65
|
+
if (seen.has(c.url)) return false;
|
|
66
|
+
seen.add(c.url);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toSources(citations: SearchCitation[], limit: number): SearchSource[] {
|
|
72
|
+
return citations.slice(0, limit).map(c => ({ title: c.title || c.url, url: c.url, snippet: c.citedText }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class OpenAICompatibleSearchProvider extends SearchProvider {
|
|
76
|
+
readonly id = "openai-compatible" as const;
|
|
77
|
+
readonly label = "OpenAI-compatible";
|
|
78
|
+
|
|
79
|
+
isAvailable(): boolean {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async search(params: SearchParams): Promise<SearchResponse> {
|
|
84
|
+
const ctx = params.activeModelContext;
|
|
85
|
+
if (!ctx)
|
|
86
|
+
throw new SearchProviderError(this.id, "OpenAI-compatible web search requires active model context", 400);
|
|
87
|
+
if (ctx.api !== "openai-responses" && ctx.api !== "openai-completions") {
|
|
88
|
+
throw new SearchProviderError(this.id, `OpenAI-compatible web search does not support ${ctx.api}`, 400);
|
|
89
|
+
}
|
|
90
|
+
const apiKey = await params.authStorage.getApiKey(ctx.provider, params.sessionId, {
|
|
91
|
+
baseUrl: ctx.baseUrl,
|
|
92
|
+
modelId: ctx.modelId,
|
|
93
|
+
signal: params.signal,
|
|
94
|
+
});
|
|
95
|
+
if (!apiKey) throw new SearchProviderError(this.id, `No credentials for ${ctx.provider}`, 401);
|
|
96
|
+
const model = ctx.wireModelId ?? ctx.modelId;
|
|
97
|
+
const headers = { ...(ctx.headers ?? {}), Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
|
|
98
|
+
const body =
|
|
99
|
+
ctx.api === "openai-completions"
|
|
100
|
+
? {
|
|
101
|
+
model,
|
|
102
|
+
messages: [
|
|
103
|
+
{ role: "system", content: params.systemPrompt },
|
|
104
|
+
{ role: "user", content: params.query },
|
|
105
|
+
],
|
|
106
|
+
web_search_options: {},
|
|
107
|
+
temperature: params.temperature,
|
|
108
|
+
max_tokens: params.maxOutputTokens,
|
|
109
|
+
}
|
|
110
|
+
: {
|
|
111
|
+
model,
|
|
112
|
+
input: [
|
|
113
|
+
{ role: "system", content: params.systemPrompt },
|
|
114
|
+
{ role: "user", content: params.query },
|
|
115
|
+
],
|
|
116
|
+
tools: [{ type: "web_search" }],
|
|
117
|
+
temperature: params.temperature,
|
|
118
|
+
max_output_tokens: params.maxOutputTokens,
|
|
119
|
+
};
|
|
120
|
+
const response = await fetch(endpoint(ctx.baseUrl ?? "", ctx.api), {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers,
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
signal: withHardTimeout(params.signal),
|
|
125
|
+
});
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const classified = classifyProviderHttpError(this.id, response.status, text);
|
|
129
|
+
if (classified) throw classified;
|
|
130
|
+
throw new SearchProviderError(
|
|
131
|
+
this.id,
|
|
132
|
+
`OpenAI-compatible web search error (${response.status}): ${text}`,
|
|
133
|
+
response.status,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const json = text ? JSON.parse(text) : {};
|
|
137
|
+
const citations = parseCitations(json);
|
|
138
|
+
if (citations.length === 0) {
|
|
139
|
+
throw new SearchProviderError(this.id, "OpenAI-compatible web search returned no citations", 424);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
provider: this.id,
|
|
143
|
+
answer: textFromResponse(json),
|
|
144
|
+
sources: toSources(citations, params.limit ?? params.numSearchResults ?? 10),
|
|
145
|
+
citations,
|
|
146
|
+
model,
|
|
147
|
+
requestId: json.id,
|
|
148
|
+
authMode: "api-key",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/web/search/types.ts
CHANGED
|
@@ -20,30 +20,55 @@ export type SearchProviderId =
|
|
|
20
20
|
| "parallel"
|
|
21
21
|
| "kagi"
|
|
22
22
|
| "synthetic"
|
|
23
|
-
| "searxng"
|
|
23
|
+
| "searxng"
|
|
24
|
+
| "openai-compatible";
|
|
25
|
+
|
|
26
|
+
export type WebSearchMode = "on" | "off" | "auto";
|
|
27
|
+
|
|
28
|
+
export interface ActiveSearchModelContext {
|
|
29
|
+
provider: string;
|
|
30
|
+
modelId: string;
|
|
31
|
+
wireModelId?: string;
|
|
32
|
+
api: string;
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
webSearch?: WebSearchMode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const CONFIGURABLE_SEARCH_PROVIDER_IDS = [
|
|
39
|
+
"duckduckgo",
|
|
40
|
+
"exa",
|
|
41
|
+
"brave",
|
|
42
|
+
"jina",
|
|
43
|
+
"kimi",
|
|
44
|
+
"zai",
|
|
45
|
+
"anthropic",
|
|
46
|
+
"perplexity",
|
|
47
|
+
"gemini",
|
|
48
|
+
"codex",
|
|
49
|
+
"tavily",
|
|
50
|
+
"parallel",
|
|
51
|
+
"kagi",
|
|
52
|
+
"synthetic",
|
|
53
|
+
"searxng",
|
|
54
|
+
] as const satisfies readonly SearchProviderId[];
|
|
55
|
+
|
|
56
|
+
const SEARCH_PROVIDER_IDS = [...CONFIGURABLE_SEARCH_PROVIDER_IDS, "openai-compatible"] as const;
|
|
24
57
|
|
|
25
58
|
export function isSearchProviderId(value: string): value is SearchProviderId {
|
|
26
|
-
return [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"kagi",
|
|
40
|
-
"synthetic",
|
|
41
|
-
"searxng",
|
|
42
|
-
].includes(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function isSearchProviderPreference(value: string): value is SearchProviderId | "auto" {
|
|
46
|
-
return value === "auto" || isSearchProviderId(value);
|
|
59
|
+
return (SEARCH_PROVIDER_IDS as readonly string[]).includes(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isConfigurableSearchProviderId(
|
|
63
|
+
value: string,
|
|
64
|
+
): value is (typeof CONFIGURABLE_SEARCH_PROVIDER_IDS)[number] {
|
|
65
|
+
return (CONFIGURABLE_SEARCH_PROVIDER_IDS as readonly string[]).includes(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isSearchProviderPreference(
|
|
69
|
+
value: string,
|
|
70
|
+
): value is (typeof CONFIGURABLE_SEARCH_PROVIDER_IDS)[number] | "auto" {
|
|
71
|
+
return value === "auto" || isConfigurableSearchProviderId(value);
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
/** Source returned by search (all providers) */
|