gencode-ai 0.1.0 → 0.1.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/README.md +8 -90
- package/dist/agent/agent.d.ts +1 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +8 -2
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/types.d.ts +9 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/cli/components/AllModelsSelector.d.ts +11 -0
- package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
- package/dist/cli/components/AllModelsSelector.js +153 -0
- package/dist/cli/components/AllModelsSelector.js.map +1 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +59 -25
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +1 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Messages.d.ts +15 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +41 -15
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/components/ModelSelector.d.ts +7 -7
- package/dist/cli/components/ModelSelector.d.ts.map +1 -1
- package/dist/cli/components/ModelSelector.js +116 -33
- package/dist/cli/components/ModelSelector.js.map +1 -1
- package/dist/cli/components/ProviderManager.d.ts +8 -0
- package/dist/cli/components/ProviderManager.d.ts.map +1 -0
- package/dist/cli/components/ProviderManager.js +280 -0
- package/dist/cli/components/ProviderManager.js.map +1 -0
- package/dist/cli/components/markdown.d.ts +9 -0
- package/dist/cli/components/markdown.d.ts.map +1 -0
- package/dist/cli/components/markdown.js +129 -0
- package/dist/cli/components/markdown.js.map +1 -0
- package/dist/cli/components/theme.d.ts +5 -0
- package/dist/cli/components/theme.d.ts.map +1 -1
- package/dist/cli/components/theme.js +7 -0
- package/dist/cli/components/theme.js.map +1 -1
- package/dist/cli/index.js +19 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +3 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/providers-config.d.ts +28 -0
- package/dist/config/providers-config.d.ts.map +1 -0
- package/dist/config/providers-config.js +79 -0
- package/dist/config/providers-config.js.map +1 -0
- package/dist/config/types.d.ts +31 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +14 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +5 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +13 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/registry.d.ts +66 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +158 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/search/brave.d.ts +14 -0
- package/dist/providers/search/brave.d.ts.map +1 -0
- package/dist/providers/search/brave.js +87 -0
- package/dist/providers/search/brave.js.map +1 -0
- package/dist/providers/search/exa.d.ts +12 -0
- package/dist/providers/search/exa.d.ts.map +1 -0
- package/dist/providers/search/exa.js +158 -0
- package/dist/providers/search/exa.js.map +1 -0
- package/dist/providers/search/index.d.ts +31 -0
- package/dist/providers/search/index.d.ts.map +1 -0
- package/dist/providers/search/index.js +75 -0
- package/dist/providers/search/index.js.map +1 -0
- package/dist/providers/search/serper.d.ts +14 -0
- package/dist/providers/search/serper.d.ts.map +1 -0
- package/dist/providers/search/serper.js +87 -0
- package/dist/providers/search/serper.js.map +1 -0
- package/dist/providers/search/types.d.ts +21 -0
- package/dist/providers/search/types.d.ts.map +1 -0
- package/dist/providers/search/types.js +5 -0
- package/dist/providers/search/types.js.map +1 -0
- package/dist/providers/store.d.ts +104 -0
- package/dist/providers/store.d.ts.map +1 -0
- package/dist/providers/store.js +171 -0
- package/dist/providers/store.js.map +1 -0
- package/dist/providers/types.d.ts +7 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/vertex-ai.d.ts +33 -0
- package/dist/providers/vertex-ai.d.ts.map +1 -0
- package/dist/providers/vertex-ai.js +407 -0
- package/dist/providers/vertex-ai.js.map +1 -0
- package/dist/tools/builtin/webfetch.d.ts +20 -0
- package/dist/tools/builtin/webfetch.d.ts.map +1 -0
- package/dist/tools/builtin/webfetch.js +231 -0
- package/dist/tools/builtin/webfetch.js.map +1 -0
- package/dist/tools/builtin/websearch.d.ts +17 -0
- package/dist/tools/builtin/websearch.d.ts.map +1 -0
- package/dist/tools/builtin/websearch.js +101 -0
- package/dist/tools/builtin/websearch.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +24 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +19 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +8 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/utils/ssrf.d.ts +18 -0
- package/dist/tools/utils/ssrf.d.ts.map +1 -0
- package/dist/tools/utils/ssrf.js +70 -0
- package/dist/tools/utils/ssrf.js.map +1 -0
- package/docs/README.md +5 -4
- package/docs/proposals/0001-web-fetch-tool.md +32 -2
- package/docs/proposals/0002-web-search-tool.md +59 -2
- package/docs/proposals/0041-configuration-system.md +556 -0
- package/docs/proposals/README.md +3 -2
- package/docs/providers.md +220 -0
- package/package.json +7 -2
- package/src/agent/agent.ts +9 -2
- package/src/agent/types.ts +9 -1
- package/src/cli/components/App.tsx +72 -23
- package/src/cli/components/CommandSuggestions.tsx +1 -0
- package/src/cli/components/Messages.tsx +117 -29
- package/src/cli/components/ModelSelector.tsx +169 -52
- package/src/cli/components/ProviderManager.tsx +534 -0
- package/src/cli/components/markdown.ts +157 -0
- package/src/cli/components/theme.ts +7 -0
- package/src/cli/index.tsx +22 -7
- package/src/config/index.ts +3 -2
- package/src/config/providers-config.ts +85 -0
- package/src/config/types.ts +35 -1
- package/src/providers/gemini.ts +20 -4
- package/src/providers/index.ts +18 -3
- package/src/providers/registry.ts +198 -0
- package/src/providers/search/brave.ts +132 -0
- package/src/providers/search/exa.ts +217 -0
- package/src/providers/search/index.ts +79 -0
- package/src/providers/search/serper.ts +133 -0
- package/src/providers/search/types.ts +24 -0
- package/src/providers/store.ts +216 -0
- package/src/providers/types.ts +9 -1
- package/src/providers/vertex-ai.ts +594 -0
- package/src/tools/builtin/webfetch.ts +264 -0
- package/src/tools/builtin/websearch.ts +117 -0
- package/src/tools/index.ts +24 -2
- package/src/tools/types.ts +20 -0
- package/src/tools/utils/ssrf.ts +79 -0
- package/CLAUDE.md +0 -70
|
@@ -14,6 +14,7 @@ export const colors = {
|
|
|
14
14
|
textMuted: '#64748B', // Slate 500
|
|
15
15
|
tool: '#C084FC', // Purple 400
|
|
16
16
|
separator: '#1E293B', // Slate 800
|
|
17
|
+
inputBg: '#111827', // Gray 900 - subtle background for user input
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export const icons = {
|
|
@@ -29,8 +30,14 @@ export const icons = {
|
|
|
29
30
|
info: 'ℹ',
|
|
30
31
|
// Tools
|
|
31
32
|
tool: '⚡', // Lightning for tools
|
|
33
|
+
fetch: '●', // Filled circle for fetch (Claude Code style)
|
|
32
34
|
arrow: '→',
|
|
33
35
|
// UI
|
|
34
36
|
thinking: '✱', // Star for thinking state
|
|
35
37
|
cursor: '▋',
|
|
38
|
+
// Selection
|
|
39
|
+
radio: '●', // Filled radio for selected
|
|
40
|
+
radioEmpty: '○', // Empty radio for unselected
|
|
41
|
+
// Tree connectors
|
|
42
|
+
treeEnd: '└', // Tree end connector for tool results
|
|
36
43
|
};
|
package/src/cli/index.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from './components/App.js';
|
|
11
11
|
import type { AgentConfig } from '../agent/types.js';
|
|
12
|
-
import { SettingsManager, type Settings } from '../config/index.js';
|
|
12
|
+
import { SettingsManager, ProvidersConfigManager, type Settings, type ProviderName } from '../config/index.js';
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Proxy Setup
|
|
@@ -31,12 +31,17 @@ async function setupProxy(): Promise<void> {
|
|
|
31
31
|
// ============================================================================
|
|
32
32
|
// Configuration
|
|
33
33
|
// ============================================================================
|
|
34
|
-
function detectConfig(settings: Settings): AgentConfig {
|
|
35
|
-
let provider:
|
|
34
|
+
function detectConfig(settings: Settings, providersConfig: ProvidersConfigManager): AgentConfig {
|
|
35
|
+
let provider: ProviderName = 'gemini';
|
|
36
36
|
let model = 'gemini-2.0-flash';
|
|
37
37
|
|
|
38
|
+
// Check for explicit Vertex AI enablement first (highest priority for auto-detect)
|
|
39
|
+
if (process.env.GENCODE_USE_VERTEX === '1' || process.env.CLAUDE_CODE_USE_VERTEX === '1') {
|
|
40
|
+
provider = 'vertex-ai';
|
|
41
|
+
model = process.env.VERTEX_AI_MODEL ?? 'claude-sonnet-4-5@20250929';
|
|
42
|
+
}
|
|
38
43
|
// Auto-detect from API keys
|
|
39
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
44
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
40
45
|
provider = 'anthropic';
|
|
41
46
|
model = 'claude-sonnet-4-20250514';
|
|
42
47
|
} else if (process.env.OPENAI_API_KEY) {
|
|
@@ -49,7 +54,7 @@ function detectConfig(settings: Settings): AgentConfig {
|
|
|
49
54
|
|
|
50
55
|
// Override from env vars
|
|
51
56
|
if (process.env.GENCODE_PROVIDER) {
|
|
52
|
-
provider = process.env.GENCODE_PROVIDER as
|
|
57
|
+
provider = process.env.GENCODE_PROVIDER as ProviderName;
|
|
53
58
|
}
|
|
54
59
|
if (process.env.GENCODE_MODEL) {
|
|
55
60
|
model = process.env.GENCODE_MODEL;
|
|
@@ -61,6 +66,13 @@ function detectConfig(settings: Settings): AgentConfig {
|
|
|
61
66
|
}
|
|
62
67
|
if (settings.model) {
|
|
63
68
|
model = settings.model;
|
|
69
|
+
// Auto-infer provider from model using providers.json (if not explicitly set)
|
|
70
|
+
if (!settings.provider) {
|
|
71
|
+
const inferredProvider = providersConfig.inferProvider(model);
|
|
72
|
+
if (inferredProvider) {
|
|
73
|
+
provider = inferredProvider;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
return {
|
|
@@ -114,11 +126,14 @@ async function main() {
|
|
|
114
126
|
|
|
115
127
|
await setupProxy();
|
|
116
128
|
|
|
117
|
-
// Load saved settings
|
|
129
|
+
// Load saved settings and providers config
|
|
118
130
|
const settingsManager = new SettingsManager();
|
|
119
131
|
const settings = await settingsManager.load();
|
|
120
132
|
|
|
121
|
-
const
|
|
133
|
+
const providersConfig = new ProvidersConfigManager();
|
|
134
|
+
await providersConfig.load();
|
|
135
|
+
|
|
136
|
+
const config = detectConfig(settings, providersConfig);
|
|
122
137
|
|
|
123
138
|
// Render the Ink app
|
|
124
139
|
render(
|
package/src/config/index.ts
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { SettingsManager } from './manager.js';
|
|
6
|
-
export
|
|
7
|
-
export {
|
|
6
|
+
export { ProvidersConfigManager } from './providers-config.js';
|
|
7
|
+
export type { Settings, SettingsManagerOptions, ProviderName, ProvidersConfig } from './types.js';
|
|
8
|
+
export { DEFAULT_SETTINGS_DIR, SETTINGS_FILE_NAME, PROVIDERS_FILE_NAME } from './types.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Providers Config Manager - Reads providers.json for model-to-provider mapping
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import type { ProvidersConfig, ProviderName } from './types.js';
|
|
9
|
+
import { DEFAULT_SETTINGS_DIR, PROVIDERS_FILE_NAME } from './types.js';
|
|
10
|
+
|
|
11
|
+
export class ProvidersConfigManager {
|
|
12
|
+
private settingsDir: string;
|
|
13
|
+
private providersPath: string;
|
|
14
|
+
private config: ProvidersConfig | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(settingsDir?: string) {
|
|
17
|
+
const dir = settingsDir ?? DEFAULT_SETTINGS_DIR;
|
|
18
|
+
this.settingsDir = dir.replace('~', os.homedir());
|
|
19
|
+
this.providersPath = path.join(this.settingsDir, PROVIDERS_FILE_NAME);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load providers config from disk
|
|
24
|
+
*/
|
|
25
|
+
async load(): Promise<ProvidersConfig | null> {
|
|
26
|
+
try {
|
|
27
|
+
const content = await fs.readFile(this.providersPath, 'utf-8');
|
|
28
|
+
this.config = JSON.parse(content);
|
|
29
|
+
return this.config;
|
|
30
|
+
} catch {
|
|
31
|
+
// File doesn't exist or is invalid
|
|
32
|
+
this.config = null;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get cached config (call load() first)
|
|
39
|
+
*/
|
|
40
|
+
get(): ProvidersConfig | null {
|
|
41
|
+
return this.config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Infer provider from model ID using cached models in providers.json
|
|
46
|
+
* Returns undefined if model not found in any provider's cached list
|
|
47
|
+
*/
|
|
48
|
+
inferProvider(modelId: string): ProviderName | undefined {
|
|
49
|
+
if (!this.config?.models) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const [providerKey, providerModels] of Object.entries(this.config.models)) {
|
|
54
|
+
const found = providerModels.list?.some((m) => m.id === modelId);
|
|
55
|
+
if (found) {
|
|
56
|
+
// Map provider key to ProviderName
|
|
57
|
+
// Note: 'anthropic' in providers.json might use vertex connection
|
|
58
|
+
if (providerKey === 'gemini') {
|
|
59
|
+
return 'gemini';
|
|
60
|
+
} else if (providerKey === 'anthropic') {
|
|
61
|
+
// Check connection method to determine if vertex or direct
|
|
62
|
+
const connection = this.config.connections?.[providerKey];
|
|
63
|
+
if (connection?.method === 'vertex') {
|
|
64
|
+
return 'vertex-ai';
|
|
65
|
+
}
|
|
66
|
+
return 'anthropic';
|
|
67
|
+
} else if (providerKey === 'openai') {
|
|
68
|
+
return 'openai';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all model IDs for a provider
|
|
78
|
+
*/
|
|
79
|
+
getModelIds(provider: string): string[] {
|
|
80
|
+
if (!this.config?.models?.[provider]) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return this.config.models[provider].list?.map((m) => m.id) ?? [];
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/config/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Settings Types - User settings persistence (Claude Code style)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type ProviderName = 'openai' | 'anthropic' | 'gemini';
|
|
5
|
+
export type ProviderName = 'openai' | 'anthropic' | 'gemini' | 'vertex-ai';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Settings file structure (~/.gencode/settings.json)
|
|
@@ -23,3 +23,37 @@ export interface SettingsManagerOptions {
|
|
|
23
23
|
|
|
24
24
|
export const DEFAULT_SETTINGS_DIR = '~/.gencode';
|
|
25
25
|
export const SETTINGS_FILE_NAME = 'settings.json';
|
|
26
|
+
export const PROVIDERS_FILE_NAME = 'providers.json';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Provider connection info
|
|
30
|
+
*/
|
|
31
|
+
export interface ProviderConnection {
|
|
32
|
+
method: 'api_key' | 'vertex' | 'oauth';
|
|
33
|
+
connectedAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cached model info
|
|
38
|
+
*/
|
|
39
|
+
export interface CachedModel {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cached models for a provider
|
|
47
|
+
*/
|
|
48
|
+
export interface ProviderModels {
|
|
49
|
+
cachedAt: string;
|
|
50
|
+
list: CachedModel[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Providers config file structure (~/.gencode/providers.json)
|
|
55
|
+
*/
|
|
56
|
+
export interface ProvidersConfig {
|
|
57
|
+
connections: Record<string, ProviderConnection>;
|
|
58
|
+
models: Record<string, ProviderModels>;
|
|
59
|
+
}
|
package/src/providers/gemini.ts
CHANGED
|
@@ -65,7 +65,12 @@ export class GeminiProvider implements LLMProvider {
|
|
|
65
65
|
const result = await model.generateContentStream({ contents });
|
|
66
66
|
|
|
67
67
|
let textContent = '';
|
|
68
|
-
const functionCalls: Array<{
|
|
68
|
+
const functionCalls: Array<{
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
args: Record<string, unknown>;
|
|
72
|
+
thoughtSignature?: string;
|
|
73
|
+
}> = [];
|
|
69
74
|
let callIndex = 0;
|
|
70
75
|
|
|
71
76
|
for await (const chunk of result.stream) {
|
|
@@ -78,10 +83,13 @@ export class GeminiProvider implements LLMProvider {
|
|
|
78
83
|
} else if ('functionCall' in part && part.functionCall) {
|
|
79
84
|
const fc = part.functionCall;
|
|
80
85
|
const id = `call_${callIndex++}`;
|
|
86
|
+
// Capture thoughtSignature for Gemini 3+ models
|
|
87
|
+
const partAny = part as { thoughtSignature?: string };
|
|
81
88
|
functionCalls.push({
|
|
82
89
|
id,
|
|
83
90
|
name: fc.name,
|
|
84
91
|
args: (fc.args as Record<string, unknown>) ?? {},
|
|
92
|
+
thoughtSignature: partAny.thoughtSignature,
|
|
85
93
|
});
|
|
86
94
|
yield { type: 'tool_start', id, name: fc.name };
|
|
87
95
|
yield { type: 'tool_input', id, input: JSON.stringify(fc.args) };
|
|
@@ -100,6 +108,7 @@ export class GeminiProvider implements LLMProvider {
|
|
|
100
108
|
id: fc.id,
|
|
101
109
|
name: fc.name,
|
|
102
110
|
input: fc.args,
|
|
111
|
+
thoughtSignature: fc.thoughtSignature,
|
|
103
112
|
});
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -152,13 +161,17 @@ export class GeminiProvider implements LLMProvider {
|
|
|
152
161
|
if (item.type === 'text') {
|
|
153
162
|
parts.push({ text: item.text });
|
|
154
163
|
} else if (item.type === 'tool_use' && role === 'model') {
|
|
155
|
-
// Function call from model
|
|
156
|
-
|
|
164
|
+
// Function call from model - include thoughtSignature for Gemini 3+
|
|
165
|
+
const fcPart: Part & { thoughtSignature?: string } = {
|
|
157
166
|
functionCall: {
|
|
158
167
|
name: item.name,
|
|
159
168
|
args: item.input,
|
|
160
169
|
},
|
|
161
|
-
}
|
|
170
|
+
};
|
|
171
|
+
if (item.thoughtSignature) {
|
|
172
|
+
fcPart.thoughtSignature = item.thoughtSignature;
|
|
173
|
+
}
|
|
174
|
+
parts.push(fcPart as Part);
|
|
162
175
|
} else if (item.type === 'tool_result' && role === 'user') {
|
|
163
176
|
// Function response
|
|
164
177
|
parts.push({
|
|
@@ -234,11 +247,14 @@ export class GeminiProvider implements LLMProvider {
|
|
|
234
247
|
if ('text' in part && part.text) {
|
|
235
248
|
content.push({ type: 'text', text: part.text });
|
|
236
249
|
} else if ('functionCall' in part && part.functionCall) {
|
|
250
|
+
// Capture thoughtSignature for Gemini 3+ models
|
|
251
|
+
const partAny = part as { thoughtSignature?: string };
|
|
237
252
|
content.push({
|
|
238
253
|
type: 'tool_use',
|
|
239
254
|
id: `call_${callIndex++}`,
|
|
240
255
|
name: part.functionCall.name,
|
|
241
256
|
input: (part.functionCall.args as Record<string, unknown>) ?? {},
|
|
257
|
+
thoughtSignature: partAny.thoughtSignature,
|
|
242
258
|
});
|
|
243
259
|
}
|
|
244
260
|
}
|
package/src/providers/index.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LLM Providers - Unified interface for OpenAI, Anthropic, and
|
|
2
|
+
* LLM Providers - Unified interface for OpenAI, Anthropic, Gemini, and Vertex AI
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export * from './types.js';
|
|
6
6
|
export { OpenAIProvider } from './openai.js';
|
|
7
7
|
export { AnthropicProvider } from './anthropic.js';
|
|
8
8
|
export { GeminiProvider } from './gemini.js';
|
|
9
|
+
export { VertexAIProvider } from './vertex-ai.js';
|
|
9
10
|
|
|
10
|
-
import type { LLMProvider, OpenAIConfig, AnthropicConfig, GeminiConfig } from './types.js';
|
|
11
|
+
import type { LLMProvider, OpenAIConfig, AnthropicConfig, GeminiConfig, VertexAIConfig } from './types.js';
|
|
11
12
|
import { OpenAIProvider } from './openai.js';
|
|
12
13
|
import { AnthropicProvider } from './anthropic.js';
|
|
13
14
|
import { GeminiProvider } from './gemini.js';
|
|
15
|
+
import { VertexAIProvider } from './vertex-ai.js';
|
|
14
16
|
|
|
15
|
-
export type ProviderName = 'openai' | 'anthropic' | 'gemini';
|
|
17
|
+
export type ProviderName = 'openai' | 'anthropic' | 'gemini' | 'vertex-ai';
|
|
16
18
|
export type ProviderConfigMap = {
|
|
17
19
|
openai: OpenAIConfig;
|
|
18
20
|
anthropic: AnthropicConfig;
|
|
19
21
|
gemini: GeminiConfig;
|
|
22
|
+
'vertex-ai': VertexAIConfig;
|
|
20
23
|
};
|
|
21
24
|
|
|
22
25
|
export interface CreateProviderOptions<T extends ProviderName = ProviderName> {
|
|
@@ -35,6 +38,8 @@ export function createProvider(options: CreateProviderOptions): LLMProvider {
|
|
|
35
38
|
return new AnthropicProvider(options.config as AnthropicConfig);
|
|
36
39
|
case 'gemini':
|
|
37
40
|
return new GeminiProvider(options.config as GeminiConfig);
|
|
41
|
+
case 'vertex-ai':
|
|
42
|
+
return new VertexAIProvider(options.config as VertexAIConfig);
|
|
38
43
|
default:
|
|
39
44
|
throw new Error(`Unknown provider: ${options.provider}`);
|
|
40
45
|
}
|
|
@@ -46,6 +51,11 @@ export function createProvider(options: CreateProviderOptions): LLMProvider {
|
|
|
46
51
|
export function inferProvider(model: string): ProviderName {
|
|
47
52
|
const modelLower = model.toLowerCase();
|
|
48
53
|
|
|
54
|
+
// Vertex AI models (Claude models with @ version suffix like claude-sonnet-4-5@20250929)
|
|
55
|
+
if (modelLower.includes('claude') && modelLower.includes('@')) {
|
|
56
|
+
return 'vertex-ai';
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
// OpenAI models
|
|
50
60
|
if (
|
|
51
61
|
modelLower.includes('gpt') ||
|
|
@@ -94,4 +104,9 @@ export const ModelAliases: Record<string, { provider: ProviderName; model: strin
|
|
|
94
104
|
'gemini-2.0-flash': { provider: 'gemini', model: 'gemini-2.0-flash' },
|
|
95
105
|
'gemini-1.5-pro': { provider: 'gemini', model: 'gemini-1.5-pro' },
|
|
96
106
|
'gemini-1.5-flash': { provider: 'gemini', model: 'gemini-1.5-flash' },
|
|
107
|
+
|
|
108
|
+
// Vertex AI (Claude on GCP)
|
|
109
|
+
'vertex-sonnet': { provider: 'vertex-ai', model: 'claude-sonnet-4-5@20250929' },
|
|
110
|
+
'vertex-haiku': { provider: 'vertex-ai', model: 'claude-haiku-4-5@20251001' },
|
|
111
|
+
'vertex-opus': { provider: 'vertex-ai', model: 'claude-opus-4-1@20250805' },
|
|
97
112
|
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Registry - Provider definitions with connection options
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ProviderName } from './index.js';
|
|
6
|
+
import type { SearchProviderName } from './search/types.js';
|
|
7
|
+
|
|
8
|
+
export interface ConnectionOption {
|
|
9
|
+
method: string;
|
|
10
|
+
name: string;
|
|
11
|
+
envVars: string[];
|
|
12
|
+
description?: string;
|
|
13
|
+
providerImpl?: ProviderName; // Override provider implementation (e.g., vertex-ai for Anthropic via GCP)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderDefinition {
|
|
17
|
+
id: ProviderName;
|
|
18
|
+
name: string;
|
|
19
|
+
popularity: number; // Lower = more popular, used for sorting
|
|
20
|
+
connections: ConnectionOption[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SearchProviderDefinition {
|
|
24
|
+
id: SearchProviderName;
|
|
25
|
+
name: string;
|
|
26
|
+
popularity: number;
|
|
27
|
+
connections: ConnectionOption[];
|
|
28
|
+
requiresKey: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* All supported providers with their connection options
|
|
33
|
+
*/
|
|
34
|
+
export const PROVIDERS: ProviderDefinition[] = [
|
|
35
|
+
{
|
|
36
|
+
id: 'anthropic',
|
|
37
|
+
name: 'Anthropic',
|
|
38
|
+
popularity: 1,
|
|
39
|
+
connections: [
|
|
40
|
+
{
|
|
41
|
+
method: 'api_key',
|
|
42
|
+
name: 'API Key',
|
|
43
|
+
envVars: ['ANTHROPIC_API_KEY'],
|
|
44
|
+
description: 'Direct API access',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
method: 'vertex',
|
|
48
|
+
name: 'Google Vertex AI',
|
|
49
|
+
envVars: ['ANTHROPIC_VERTEX_PROJECT_ID', 'GOOGLE_CLOUD_PROJECT'],
|
|
50
|
+
description: 'Claude via GCP',
|
|
51
|
+
providerImpl: 'vertex-ai',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
method: 'bedrock',
|
|
55
|
+
name: 'Amazon Bedrock',
|
|
56
|
+
envVars: ['AWS_ACCESS_KEY_ID', 'AWS_PROFILE'],
|
|
57
|
+
description: 'Claude via AWS (coming soon)',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'openai',
|
|
63
|
+
name: 'OpenAI',
|
|
64
|
+
popularity: 2,
|
|
65
|
+
connections: [
|
|
66
|
+
{
|
|
67
|
+
method: 'api_key',
|
|
68
|
+
name: 'API Key',
|
|
69
|
+
envVars: ['OPENAI_API_KEY'],
|
|
70
|
+
description: 'Direct API access',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'gemini',
|
|
76
|
+
name: 'Google Gemini',
|
|
77
|
+
popularity: 3,
|
|
78
|
+
connections: [
|
|
79
|
+
{
|
|
80
|
+
method: 'api_key',
|
|
81
|
+
name: 'API Key',
|
|
82
|
+
envVars: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
83
|
+
description: 'Direct API access',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* All supported search providers
|
|
91
|
+
*/
|
|
92
|
+
export const SEARCH_PROVIDERS: SearchProviderDefinition[] = [
|
|
93
|
+
{
|
|
94
|
+
id: 'exa',
|
|
95
|
+
name: 'Exa AI',
|
|
96
|
+
popularity: 1,
|
|
97
|
+
connections: [
|
|
98
|
+
{
|
|
99
|
+
method: 'public',
|
|
100
|
+
name: 'Public API',
|
|
101
|
+
envVars: [],
|
|
102
|
+
description: 'No API key required',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
requiresKey: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'serper',
|
|
109
|
+
name: 'Serper.dev',
|
|
110
|
+
popularity: 2,
|
|
111
|
+
connections: [
|
|
112
|
+
{
|
|
113
|
+
method: 'api_key',
|
|
114
|
+
name: 'API Key',
|
|
115
|
+
envVars: ['SERPER_API_KEY'],
|
|
116
|
+
description: 'Google Search via Serper',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
requiresKey: true,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'brave',
|
|
123
|
+
name: 'Brave Search',
|
|
124
|
+
popularity: 3,
|
|
125
|
+
connections: [
|
|
126
|
+
{
|
|
127
|
+
method: 'api_key',
|
|
128
|
+
name: 'API Key',
|
|
129
|
+
envVars: ['BRAVE_API_KEY'],
|
|
130
|
+
description: 'Privacy-focused search',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
requiresKey: true,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get provider definition by ID
|
|
139
|
+
*/
|
|
140
|
+
export function getProvider(id: ProviderName): ProviderDefinition | undefined {
|
|
141
|
+
return PROVIDERS.find((p) => p.id === id);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get search provider definition by ID
|
|
146
|
+
*/
|
|
147
|
+
export function getSearchProvider(id: SearchProviderName): SearchProviderDefinition | undefined {
|
|
148
|
+
return SEARCH_PROVIDERS.find((p) => p.id === id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get all search providers sorted by popularity
|
|
153
|
+
*/
|
|
154
|
+
export function getSearchProvidersSorted(): SearchProviderDefinition[] {
|
|
155
|
+
return [...SEARCH_PROVIDERS].sort((a, b) => a.popularity - b.popularity);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get all providers sorted by popularity
|
|
160
|
+
*/
|
|
161
|
+
export function getProvidersSorted(): ProviderDefinition[] {
|
|
162
|
+
return [...PROVIDERS].sort((a, b) => a.popularity - b.popularity);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Helper: check if any env var in the list is set
|
|
166
|
+
const hasAnyEnvVar = (envVars: string[]) => envVars.some((v) => !!process.env[v]);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if any of the provider's env vars are set
|
|
170
|
+
*/
|
|
171
|
+
export function hasEnvVars(provider: ProviderDefinition): boolean {
|
|
172
|
+
return provider.connections.some((conn) => hasAnyEnvVar(conn.envVars));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get the first available connection method (where env vars are set)
|
|
177
|
+
*/
|
|
178
|
+
export function getAvailableConnection(
|
|
179
|
+
provider: ProviderDefinition
|
|
180
|
+
): ConnectionOption | undefined {
|
|
181
|
+
return provider.connections.find((conn) => hasAnyEnvVar(conn.envVars));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a specific connection option has its env vars set
|
|
186
|
+
*/
|
|
187
|
+
export function isConnectionReady(conn: ConnectionOption): boolean {
|
|
188
|
+
return hasAnyEnvVar(conn.envVars);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all available (ready) connections for a provider
|
|
193
|
+
*/
|
|
194
|
+
export function getAvailableConnections(
|
|
195
|
+
provider: ProviderDefinition
|
|
196
|
+
): ConnectionOption[] {
|
|
197
|
+
return provider.connections.filter((conn) => hasAnyEnvVar(conn.envVars));
|
|
198
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brave Search Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses Brave Search API (same as Claude Code).
|
|
5
|
+
* Requires BRAVE_API_KEY environment variable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SearchProvider, SearchResult, SearchOptions } from './types.js';
|
|
9
|
+
|
|
10
|
+
const API_CONFIG = {
|
|
11
|
+
BASE_URL: 'https://api.search.brave.com',
|
|
12
|
+
ENDPOINT: '/res/v1/web/search',
|
|
13
|
+
DEFAULT_NUM_RESULTS: 10,
|
|
14
|
+
DEFAULT_TIMEOUT: 10000,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
interface BraveWebResult {
|
|
18
|
+
title: string;
|
|
19
|
+
url: string;
|
|
20
|
+
description: string;
|
|
21
|
+
is_source_local?: boolean;
|
|
22
|
+
is_source_both?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface BraveResponse {
|
|
26
|
+
web?: {
|
|
27
|
+
results: BraveWebResult[];
|
|
28
|
+
};
|
|
29
|
+
query?: {
|
|
30
|
+
original: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Filter results by allowed/blocked domains
|
|
36
|
+
*/
|
|
37
|
+
function filterByDomain(results: SearchResult[], options?: SearchOptions): SearchResult[] {
|
|
38
|
+
if (!options?.allowedDomains?.length && !options?.blockedDomains?.length) {
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return results.filter((result) => {
|
|
43
|
+
try {
|
|
44
|
+
const domain = new URL(result.url).hostname;
|
|
45
|
+
|
|
46
|
+
if (options.allowedDomains?.length) {
|
|
47
|
+
return options.allowedDomains.some(
|
|
48
|
+
(allowed) => domain === allowed || domain.endsWith('.' + allowed)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.blockedDomains?.length) {
|
|
53
|
+
return !options.blockedDomains.some(
|
|
54
|
+
(blocked) => domain === blocked || domain.endsWith('.' + blocked)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class BraveProvider implements SearchProvider {
|
|
66
|
+
readonly name = 'brave' as const;
|
|
67
|
+
private apiKey: string;
|
|
68
|
+
|
|
69
|
+
constructor(apiKey?: string) {
|
|
70
|
+
this.apiKey = apiKey ?? process.env.BRAVE_API_KEY ?? '';
|
|
71
|
+
if (!this.apiKey) {
|
|
72
|
+
throw new Error('BRAVE_API_KEY environment variable is required for Brave provider');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
|
|
77
|
+
const params = new URLSearchParams({
|
|
78
|
+
q: query,
|
|
79
|
+
count: String(options?.numResults ?? API_CONFIG.DEFAULT_NUM_RESULTS),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timeoutId = setTimeout(
|
|
84
|
+
() => controller.abort(),
|
|
85
|
+
options?.timeout ?? API_CONFIG.DEFAULT_TIMEOUT
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const signals = options?.abortSignal
|
|
90
|
+
? [controller.signal, options.abortSignal]
|
|
91
|
+
: [controller.signal];
|
|
92
|
+
|
|
93
|
+
const response = await fetch(
|
|
94
|
+
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINT}?${params.toString()}`,
|
|
95
|
+
{
|
|
96
|
+
method: 'GET',
|
|
97
|
+
headers: {
|
|
98
|
+
Accept: 'application/json',
|
|
99
|
+
'Accept-Encoding': 'gzip',
|
|
100
|
+
'X-Subscription-Token': this.apiKey,
|
|
101
|
+
},
|
|
102
|
+
signal: AbortSignal.any(signals),
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const errorText = await response.text();
|
|
110
|
+
throw new Error(`Brave search error (${response.status}): ${errorText}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const data = (await response.json()) as BraveResponse;
|
|
114
|
+
|
|
115
|
+
const results: SearchResult[] = (data.web?.results || []).map((item) => ({
|
|
116
|
+
title: item.title,
|
|
117
|
+
url: item.url,
|
|
118
|
+
snippet: item.description,
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
return filterByDomain(results, options);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
clearTimeout(timeoutId);
|
|
124
|
+
|
|
125
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
126
|
+
throw new Error('Search request timed out');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|