overarch-linux-arm64 0.8.0
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/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/bin/overarch.d.ts +2 -0
- package/dist/bin/overarch.js +178 -0
- package/dist/src/agent/context.d.ts +14 -0
- package/dist/src/agent/context.js +200 -0
- package/dist/src/agent/context_compressor.d.ts +21 -0
- package/dist/src/agent/context_compressor.js +90 -0
- package/dist/src/agent/manifest.d.ts +22 -0
- package/dist/src/agent/manifest.js +131 -0
- package/dist/src/agent/planner.d.ts +38 -0
- package/dist/src/agent/planner.js +118 -0
- package/dist/src/agent/runtime.d.ts +59 -0
- package/dist/src/agent/runtime.js +400 -0
- package/dist/src/cli/approval.d.ts +13 -0
- package/dist/src/cli/approval.js +71 -0
- package/dist/src/cli/args.d.ts +15 -0
- package/dist/src/cli/args.js +47 -0
- package/dist/src/cli/commands/audit.d.ts +10 -0
- package/dist/src/cli/commands/audit.js +79 -0
- package/dist/src/cli/commands/companion.d.ts +1 -0
- package/dist/src/cli/commands/companion.js +143 -0
- package/dist/src/cli/commands/config.d.ts +1 -0
- package/dist/src/cli/commands/config.js +135 -0
- package/dist/src/cli/commands/doctor.d.ts +1 -0
- package/dist/src/cli/commands/doctor.js +217 -0
- package/dist/src/cli/commands/init.d.ts +1 -0
- package/dist/src/cli/commands/init.js +134 -0
- package/dist/src/cli/commands/memory.d.ts +2 -0
- package/dist/src/cli/commands/memory.js +98 -0
- package/dist/src/cli/commands/onboarding.d.ts +3 -0
- package/dist/src/cli/commands/onboarding.js +223 -0
- package/dist/src/cli/commands/sessions.d.ts +1 -0
- package/dist/src/cli/commands/sessions.js +22 -0
- package/dist/src/cli/commands/status.d.ts +1 -0
- package/dist/src/cli/commands/status.js +37 -0
- package/dist/src/cli/commands/update.d.ts +9 -0
- package/dist/src/cli/commands/update.js +105 -0
- package/dist/src/cli/repl.d.ts +3 -0
- package/dist/src/cli/repl.js +821 -0
- package/dist/src/cli/ui.d.ts +53 -0
- package/dist/src/cli/ui.js +215 -0
- package/dist/src/companion/petdex_adapter.d.ts +31 -0
- package/dist/src/companion/petdex_adapter.js +171 -0
- package/dist/src/companion/petdex_bridge.d.ts +65 -0
- package/dist/src/companion/petdex_bridge.js +356 -0
- package/dist/src/companion/types.d.ts +57 -0
- package/dist/src/companion/types.js +11 -0
- package/dist/src/config/index.d.ts +24 -0
- package/dist/src/config/index.js +237 -0
- package/dist/src/config/types.d.ts +58 -0
- package/dist/src/config/types.js +7 -0
- package/dist/src/core/diagnostics.d.ts +48 -0
- package/dist/src/core/diagnostics.js +115 -0
- package/dist/src/core/index.d.ts +23 -0
- package/dist/src/core/index.js +23 -0
- package/dist/src/core/mission.d.ts +55 -0
- package/dist/src/core/mission.js +173 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/learning/memory_manager.d.ts +26 -0
- package/dist/src/learning/memory_manager.js +211 -0
- package/dist/src/learning/types.d.ts +54 -0
- package/dist/src/learning/types.js +1 -0
- package/dist/src/mcp/registry.d.ts +17 -0
- package/dist/src/mcp/registry.js +97 -0
- package/dist/src/mcp/types.d.ts +13 -0
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/providers/anthropic.d.ts +10 -0
- package/dist/src/providers/anthropic.js +95 -0
- package/dist/src/providers/base.d.ts +15 -0
- package/dist/src/providers/base.js +108 -0
- package/dist/src/providers/factory.d.ts +28 -0
- package/dist/src/providers/factory.js +142 -0
- package/dist/src/providers/gemini.d.ts +10 -0
- package/dist/src/providers/gemini.js +97 -0
- package/dist/src/providers/openai.d.ts +9 -0
- package/dist/src/providers/openai.js +85 -0
- package/dist/src/providers/openrouter.d.ts +5 -0
- package/dist/src/providers/openrouter.js +14 -0
- package/dist/src/providers/registry.d.ts +19 -0
- package/dist/src/providers/registry.js +209 -0
- package/dist/src/providers/types.d.ts +59 -0
- package/dist/src/providers/types.js +1 -0
- package/dist/src/providers/validator.d.ts +24 -0
- package/dist/src/providers/validator.js +161 -0
- package/dist/src/security/audit.d.ts +56 -0
- package/dist/src/security/audit.js +154 -0
- package/dist/src/security/guardrails.d.ts +9 -0
- package/dist/src/security/guardrails.js +32 -0
- package/dist/src/security/permissions.d.ts +12 -0
- package/dist/src/security/permissions.js +68 -0
- package/dist/src/security/policy.d.ts +220 -0
- package/dist/src/security/policy.js +720 -0
- package/dist/src/session/manager.d.ts +22 -0
- package/dist/src/session/manager.js +172 -0
- package/dist/src/session/types.d.ts +36 -0
- package/dist/src/session/types.js +1 -0
- package/dist/src/tools/checkpoint.d.ts +26 -0
- package/dist/src/tools/checkpoint.js +160 -0
- package/dist/src/tools/diagnostics.d.ts +2 -0
- package/dist/src/tools/diagnostics.js +125 -0
- package/dist/src/tools/diff.d.ts +14 -0
- package/dist/src/tools/diff.js +124 -0
- package/dist/src/tools/filesystem.d.ts +2 -0
- package/dist/src/tools/filesystem.js +224 -0
- package/dist/src/tools/git.d.ts +2 -0
- package/dist/src/tools/git.js +138 -0
- package/dist/src/tools/markdown.d.ts +33 -0
- package/dist/src/tools/markdown.js +190 -0
- package/dist/src/tools/project.d.ts +2 -0
- package/dist/src/tools/project.js +63 -0
- package/dist/src/tools/registry.d.ts +14 -0
- package/dist/src/tools/registry.js +230 -0
- package/dist/src/tools/search.d.ts +2 -0
- package/dist/src/tools/search.js +136 -0
- package/dist/src/tools/shell.d.ts +2 -0
- package/dist/src/tools/shell.js +74 -0
- package/dist/src/tools/symbols.d.ts +30 -0
- package/dist/src/tools/symbols.js +155 -0
- package/dist/src/tools/types.d.ts +28 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/web_search.d.ts +14 -0
- package/dist/src/tools/web_search.js +154 -0
- package/install.sh +29 -0
- package/overarch +15 -0
- package/package.json +28 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { OpenAIProvider } from './openai.js';
|
|
2
|
+
import { AnthropicProvider } from './anthropic.js';
|
|
3
|
+
import { GeminiProvider } from './gemini.js';
|
|
4
|
+
import { OpenRouterProvider } from './openrouter.js';
|
|
5
|
+
import { CANONICAL_MODELS, SUPPORTED_PROVIDERS, normalizeModel } from './registry.js';
|
|
6
|
+
export { CANONICAL_MODELS, SUPPORTED_PROVIDERS, normalizeModel };
|
|
7
|
+
import { getApiKey } from '../config/index.js';
|
|
8
|
+
export const MODEL_REGISTRY = CANONICAL_MODELS;
|
|
9
|
+
export class OfflineProvider {
|
|
10
|
+
name = 'offline';
|
|
11
|
+
async complete(request) {
|
|
12
|
+
return {
|
|
13
|
+
content: 'Offline mode is currently active. Connect an AI provider with `/setup` or `/config provider` to enable AI completions.',
|
|
14
|
+
toolCalls: [],
|
|
15
|
+
finishReason: 'stop'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async *stream(request) {
|
|
19
|
+
yield {
|
|
20
|
+
type: 'text',
|
|
21
|
+
content: 'Offline mode is currently active. Connect an AI provider with `/setup` or `/config provider` to enable AI completions.'
|
|
22
|
+
};
|
|
23
|
+
yield { type: 'done' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createProvider(providerName, config = {}) {
|
|
27
|
+
const normProvider = providerName.toLowerCase().trim();
|
|
28
|
+
if (normProvider === 'none' || normProvider === 'offline') {
|
|
29
|
+
return new OfflineProvider();
|
|
30
|
+
}
|
|
31
|
+
const apiKey = config.apiKey || getApiKey(normProvider);
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
throw new Error(`API key not found for provider: ${providerName}`);
|
|
34
|
+
}
|
|
35
|
+
switch (normProvider) {
|
|
36
|
+
case 'openai':
|
|
37
|
+
return new OpenAIProvider(apiKey, config.baseUrl);
|
|
38
|
+
case 'anthropic':
|
|
39
|
+
return new AnthropicProvider(apiKey, config.baseUrl);
|
|
40
|
+
case 'gemini':
|
|
41
|
+
case 'google':
|
|
42
|
+
return new GeminiProvider(apiKey, config.baseUrl);
|
|
43
|
+
case 'openrouter':
|
|
44
|
+
return new OpenRouterProvider(apiKey, config.baseUrl);
|
|
45
|
+
default:
|
|
46
|
+
// Generic OpenAI-compatible fallback (e.g. Ollama, Groq, DeepSeek, vLLM)
|
|
47
|
+
return new OpenAIProvider(apiKey, config.baseUrl, normProvider);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves a raw model string, alias, or provider/model into a canonical structure.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveModel(modelId, preferredProvider) {
|
|
54
|
+
return normalizeModel(modelId, preferredProvider);
|
|
55
|
+
}
|
|
56
|
+
export function listModels(providerFilter) {
|
|
57
|
+
if (!providerFilter) {
|
|
58
|
+
return CANONICAL_MODELS.filter(m => m.provider !== 'none');
|
|
59
|
+
}
|
|
60
|
+
return CANONICAL_MODELS.filter(m => m.provider.toLowerCase() === providerFilter.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
export function listProviders() {
|
|
63
|
+
return SUPPORTED_PROVIDERS;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validates whether an API key is structurally and functionally plausible.
|
|
67
|
+
*/
|
|
68
|
+
export async function validateProviderKey(providerName, apiKey) {
|
|
69
|
+
const cleanKey = apiKey.trim();
|
|
70
|
+
if (!cleanKey) {
|
|
71
|
+
return { valid: false, error: 'Key cannot be empty' };
|
|
72
|
+
}
|
|
73
|
+
const norm = providerName.toLowerCase();
|
|
74
|
+
// Basic syntax/prefix heuristics
|
|
75
|
+
if (norm === 'anthropic' && !cleanKey.startsWith('sk-ant-') && cleanKey.length < 20) {
|
|
76
|
+
return { valid: false, error: 'Anthropic keys typically begin with sk-ant-' };
|
|
77
|
+
}
|
|
78
|
+
if (norm === 'openai' && !cleanKey.startsWith('sk-') && cleanKey.length < 20) {
|
|
79
|
+
return { valid: false, error: 'OpenAI keys typically begin with sk-' };
|
|
80
|
+
}
|
|
81
|
+
// Network verification probe
|
|
82
|
+
try {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeout = setTimeout(() => controller.abort(), 6000);
|
|
85
|
+
if (norm === 'anthropic') {
|
|
86
|
+
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
87
|
+
headers: {
|
|
88
|
+
'x-api-key': cleanKey,
|
|
89
|
+
'anthropic-version': '2023-06-01'
|
|
90
|
+
},
|
|
91
|
+
signal: controller.signal
|
|
92
|
+
});
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
if (res.status === 401 || res.status === 403) {
|
|
95
|
+
return { valid: false, error: 'Authentication failed: Invalid Anthropic API key' };
|
|
96
|
+
}
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
if (norm === 'openai') {
|
|
100
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
101
|
+
headers: { 'Authorization': `Bearer ${cleanKey}` },
|
|
102
|
+
signal: controller.signal
|
|
103
|
+
});
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
if (res.status === 401 || res.status === 403) {
|
|
106
|
+
return { valid: false, error: 'Authentication failed: Invalid OpenAI API key' };
|
|
107
|
+
}
|
|
108
|
+
return { valid: true };
|
|
109
|
+
}
|
|
110
|
+
if (norm === 'gemini' || norm === 'google') {
|
|
111
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${cleanKey}`, {
|
|
112
|
+
signal: controller.signal
|
|
113
|
+
});
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
if (res.status === 400 || res.status === 401 || res.status === 403) {
|
|
116
|
+
return { valid: false, error: 'Authentication failed: Invalid Gemini API key' };
|
|
117
|
+
}
|
|
118
|
+
return { valid: true };
|
|
119
|
+
}
|
|
120
|
+
if (norm === 'openrouter') {
|
|
121
|
+
const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
122
|
+
headers: { 'Authorization': `Bearer ${cleanKey}` },
|
|
123
|
+
signal: controller.signal
|
|
124
|
+
});
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
if (res.status === 401 || res.status === 403) {
|
|
127
|
+
return { valid: false, error: 'Authentication failed: Invalid OpenRouter API key' };
|
|
128
|
+
}
|
|
129
|
+
return { valid: true };
|
|
130
|
+
}
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
return { valid: true };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err.name === 'AbortError') {
|
|
136
|
+
// If network times out, don't block user if key looks well-formed
|
|
137
|
+
return { valid: true };
|
|
138
|
+
}
|
|
139
|
+
// Network unreachable or offline during probe
|
|
140
|
+
return { valid: true };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
import { CompletionRequest, CompletionResponse, StreamChunk } from './types.js';
|
|
3
|
+
export declare class GeminiProvider extends BaseProvider {
|
|
4
|
+
constructor(apiKey: string, baseUrl?: string);
|
|
5
|
+
protected headers(): Record<string, string>;
|
|
6
|
+
protected getEndpoint(request: CompletionRequest): string;
|
|
7
|
+
protected buildRequestBody(request: CompletionRequest): Record<string, unknown>;
|
|
8
|
+
protected parseResponse(body: any): CompletionResponse;
|
|
9
|
+
protected parseStreamChunk(dataStr: string, buffer: Record<string, unknown>): StreamChunk | null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
export class GeminiProvider extends BaseProvider {
|
|
3
|
+
constructor(apiKey, baseUrl = 'https://generativelanguage.googleapis.com/v1beta') {
|
|
4
|
+
super('gemini', apiKey, baseUrl);
|
|
5
|
+
}
|
|
6
|
+
headers() {
|
|
7
|
+
return {
|
|
8
|
+
'Content-Type': 'application/json'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
getEndpoint(request) {
|
|
12
|
+
const base = this.baseUrl.replace(/\/$/, '');
|
|
13
|
+
if (request.stream) {
|
|
14
|
+
return `${base}/models/${request.model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
|
|
15
|
+
}
|
|
16
|
+
return `${base}/models/${request.model}:generateContent?key=${this.apiKey}`;
|
|
17
|
+
}
|
|
18
|
+
buildRequestBody(request) {
|
|
19
|
+
const contents = request.messages.map(m => {
|
|
20
|
+
// All roles should be user-facing except assistant which maps to model.
|
|
21
|
+
// Gemini API does not support a 'function' role; map tool to user.
|
|
22
|
+
let role = 'user';
|
|
23
|
+
if (m.role === 'assistant')
|
|
24
|
+
role = 'model';
|
|
25
|
+
// NOTE: do NOT map tool role to 'function'.
|
|
26
|
+
return {
|
|
27
|
+
role,
|
|
28
|
+
parts: [{ text: m.content || '' }]
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const tools = request.tools ? [{
|
|
32
|
+
functionDeclarations: request.tools.map(t => ({
|
|
33
|
+
name: t.function.name,
|
|
34
|
+
description: t.function.description,
|
|
35
|
+
parameters: t.function.parameters
|
|
36
|
+
}))
|
|
37
|
+
}] : undefined;
|
|
38
|
+
return {
|
|
39
|
+
contents,
|
|
40
|
+
...(tools ? { tools } : {}),
|
|
41
|
+
generationConfig: {
|
|
42
|
+
...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
|
|
43
|
+
...(request.maxTokens !== undefined ? { maxOutputTokens: request.maxTokens } : {})
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
parseResponse(body) {
|
|
48
|
+
const candidate = body.candidates?.[0];
|
|
49
|
+
const parts = candidate?.content?.parts || [];
|
|
50
|
+
let content = '';
|
|
51
|
+
const toolCalls = [];
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (part.text)
|
|
54
|
+
content += part.text;
|
|
55
|
+
if (part.functionCall) {
|
|
56
|
+
toolCalls.push({
|
|
57
|
+
id: `call_${Math.random().toString(36).substring(7)}`,
|
|
58
|
+
name: part.functionCall.name,
|
|
59
|
+
arguments: part.functionCall.args
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
content,
|
|
65
|
+
toolCalls,
|
|
66
|
+
finishReason: candidate?.finishReason === 'STOP' ? 'stop' : (toolCalls.length > 0 ? 'tool_calls' : 'stop')
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
parseStreamChunk(dataStr, buffer) {
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(dataStr);
|
|
72
|
+
const candidate = data.candidates?.[0];
|
|
73
|
+
if (!candidate)
|
|
74
|
+
return null;
|
|
75
|
+
const parts = candidate.content?.parts || [];
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
if (part.text) {
|
|
78
|
+
return { type: 'text', content: part.text };
|
|
79
|
+
}
|
|
80
|
+
if (part.functionCall) {
|
|
81
|
+
return {
|
|
82
|
+
type: 'tool_call_start',
|
|
83
|
+
toolCall: {
|
|
84
|
+
id: `call_${Math.random().toString(36).substring(7)}`,
|
|
85
|
+
name: part.functionCall.name,
|
|
86
|
+
arguments: JSON.stringify(part.functionCall.args)
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
import { CompletionRequest, CompletionResponse, StreamChunk } from './types.js';
|
|
3
|
+
export declare class OpenAIProvider extends BaseProvider {
|
|
4
|
+
constructor(apiKey: string, baseUrl?: string, name?: string);
|
|
5
|
+
protected getEndpoint(request: CompletionRequest): string;
|
|
6
|
+
protected buildRequestBody(request: CompletionRequest): Record<string, unknown>;
|
|
7
|
+
protected parseResponse(body: any): CompletionResponse;
|
|
8
|
+
protected parseStreamChunk(dataStr: string, buffer: Record<string, unknown>): StreamChunk | null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BaseProvider } from './base.js';
|
|
2
|
+
import { formatForOpenAI } from './validator.js';
|
|
3
|
+
export class OpenAIProvider extends BaseProvider {
|
|
4
|
+
constructor(apiKey, baseUrl = 'https://api.openai.com/v1', name = 'openai') {
|
|
5
|
+
super(name, apiKey, baseUrl);
|
|
6
|
+
}
|
|
7
|
+
getEndpoint(request) {
|
|
8
|
+
return `${this.baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
9
|
+
}
|
|
10
|
+
buildRequestBody(request) {
|
|
11
|
+
return {
|
|
12
|
+
model: request.model,
|
|
13
|
+
messages: formatForOpenAI(request.messages),
|
|
14
|
+
...(request.tools && request.tools.length > 0 ? { tools: request.tools } : {}),
|
|
15
|
+
...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
|
|
16
|
+
...(request.maxTokens !== undefined ? { max_tokens: request.maxTokens } : {}),
|
|
17
|
+
stream: request.stream
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
parseResponse(body) {
|
|
21
|
+
const choice = body.choices?.[0];
|
|
22
|
+
const message = choice?.message || {};
|
|
23
|
+
return {
|
|
24
|
+
content: message.content || '',
|
|
25
|
+
toolCalls: (message.tool_calls || []).map((tc) => {
|
|
26
|
+
let parsedArgs = tc.function?.arguments;
|
|
27
|
+
if (typeof parsedArgs === 'string') {
|
|
28
|
+
try {
|
|
29
|
+
parsedArgs = JSON.parse(parsedArgs);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
parsedArgs = {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
id: tc.id,
|
|
37
|
+
name: tc.function?.name,
|
|
38
|
+
arguments: parsedArgs || {}
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
usage: body.usage ? {
|
|
42
|
+
promptTokens: body.usage.prompt_tokens,
|
|
43
|
+
completionTokens: body.usage.completion_tokens,
|
|
44
|
+
totalTokens: body.usage.total_tokens
|
|
45
|
+
} : undefined,
|
|
46
|
+
finishReason: choice?.finish_reason || 'stop'
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
parseStreamChunk(dataStr, buffer) {
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(dataStr);
|
|
52
|
+
const choice = data.choices?.[0];
|
|
53
|
+
const delta = choice?.delta || {};
|
|
54
|
+
if (delta.content) {
|
|
55
|
+
return { type: 'text', content: delta.content };
|
|
56
|
+
}
|
|
57
|
+
if (delta.tool_calls) {
|
|
58
|
+
const tcDelta = delta.tool_calls[0];
|
|
59
|
+
if (tcDelta.id) {
|
|
60
|
+
buffer.toolCallArgs = '';
|
|
61
|
+
return {
|
|
62
|
+
type: 'tool_call_start',
|
|
63
|
+
toolCall: {
|
|
64
|
+
id: tcDelta.id,
|
|
65
|
+
name: tcDelta.function?.name
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
else if (tcDelta.function?.arguments) {
|
|
70
|
+
buffer.toolCallArgs = (buffer.toolCallArgs || '') + tcDelta.function.arguments;
|
|
71
|
+
return {
|
|
72
|
+
type: 'tool_call_args',
|
|
73
|
+
toolCall: {
|
|
74
|
+
arguments: tcDelta.function.arguments
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OpenAIProvider } from './openai.js';
|
|
2
|
+
export class OpenRouterProvider extends OpenAIProvider {
|
|
3
|
+
constructor(apiKey, baseUrl = 'https://openrouter.ai/api/v1') {
|
|
4
|
+
super(apiKey, baseUrl, 'openrouter');
|
|
5
|
+
}
|
|
6
|
+
headers() {
|
|
7
|
+
return {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
10
|
+
'HTTP-Referer': 'https://github.com/overarch/cli',
|
|
11
|
+
'X-Title': 'Overarch CLI'
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ModelInfo } from './types.js';
|
|
2
|
+
export interface ProviderDefinition {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
envVar: string;
|
|
6
|
+
defaultBaseUrl: string;
|
|
7
|
+
recommendedModels: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare const SUPPORTED_PROVIDERS: ProviderDefinition[];
|
|
10
|
+
export declare const CANONICAL_MODELS: ModelInfo[];
|
|
11
|
+
export declare const MODEL_ALIASES: Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes any model ID or alias into a canonical { provider, model, displayName } structure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeModel(input: string, preferredProvider?: string): {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
} | undefined;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
export const SUPPORTED_PROVIDERS = [
|
|
2
|
+
{
|
|
3
|
+
id: 'anthropic',
|
|
4
|
+
name: 'Anthropic Claude',
|
|
5
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
6
|
+
defaultBaseUrl: 'https://api.anthropic.com/v1',
|
|
7
|
+
recommendedModels: [
|
|
8
|
+
'claude-3-5-sonnet-20241022',
|
|
9
|
+
'claude-opus-4-6',
|
|
10
|
+
'claude-sonnet-4-20250514',
|
|
11
|
+
'claude-3-5-haiku-20241022'
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'openai',
|
|
16
|
+
name: 'OpenAI',
|
|
17
|
+
envVar: 'OPENAI_API_KEY',
|
|
18
|
+
defaultBaseUrl: 'https://api.openai.com/v1',
|
|
19
|
+
recommendedModels: [
|
|
20
|
+
'gpt-4o',
|
|
21
|
+
'gpt-4o-mini',
|
|
22
|
+
'o3-mini',
|
|
23
|
+
'o1'
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'gemini',
|
|
28
|
+
name: 'Google Gemini',
|
|
29
|
+
envVar: 'GEMINI_API_KEY',
|
|
30
|
+
defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
31
|
+
recommendedModels: [
|
|
32
|
+
'gemini-2.5-flash',
|
|
33
|
+
'gemini-2.5-pro',
|
|
34
|
+
'gemini-2.0-flash-exp',
|
|
35
|
+
'gemini-1.5-pro'
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'openrouter',
|
|
40
|
+
name: 'OpenRouter (Multi-Provider)',
|
|
41
|
+
envVar: 'OPENROUTER_API_KEY',
|
|
42
|
+
defaultBaseUrl: 'https://openrouter.ai/api/v1',
|
|
43
|
+
recommendedModels: [
|
|
44
|
+
'openrouter/auto',
|
|
45
|
+
'anthropic/claude-3.5-sonnet',
|
|
46
|
+
'openai/gpt-4o',
|
|
47
|
+
'deepseek/deepseek-chat',
|
|
48
|
+
'google/gemini-2.0-flash-001',
|
|
49
|
+
'meta-llama/llama-3.3-70b-instruct'
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
export const CANONICAL_MODELS = [
|
|
54
|
+
// Anthropic
|
|
55
|
+
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'anthropic', contextWindow: 200000, supportsTools: true },
|
|
56
|
+
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic', contextWindow: 200000, supportsTools: true },
|
|
57
|
+
{ id: 'claude-sonnet-4-20250514', name: 'Claude 4 Sonnet', provider: 'anthropic', contextWindow: 200000, supportsTools: true },
|
|
58
|
+
{ id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', provider: 'anthropic', contextWindow: 200000, supportsTools: true },
|
|
59
|
+
// OpenAI
|
|
60
|
+
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai', contextWindow: 128000, supportsTools: true },
|
|
61
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'openai', contextWindow: 128000, supportsTools: true },
|
|
62
|
+
{ id: 'o3-mini', name: 'o3-mini', provider: 'openai', contextWindow: 200000, supportsTools: true },
|
|
63
|
+
{ id: 'o1', name: 'o1', provider: 'openai', contextWindow: 200000, supportsTools: false },
|
|
64
|
+
// Gemini
|
|
65
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'gemini', contextWindow: 1048576, supportsTools: true },
|
|
66
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'gemini', contextWindow: 2097152, supportsTools: true },
|
|
67
|
+
{ id: 'gemini-2.0-flash-exp', name: 'Gemini 2.0 Flash', provider: 'gemini', contextWindow: 1048576, supportsTools: true },
|
|
68
|
+
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', provider: 'gemini', contextWindow: 2097152, supportsTools: true },
|
|
69
|
+
// OpenRouter
|
|
70
|
+
{ id: 'openrouter/auto', name: 'OpenRouter Auto (Best)', provider: 'openrouter', contextWindow: 128000, supportsTools: true },
|
|
71
|
+
{ id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet (OpenRouter)', provider: 'openrouter', contextWindow: 200000, supportsTools: true },
|
|
72
|
+
{ id: 'openai/gpt-4o', name: 'GPT-4o (OpenRouter)', provider: 'openrouter', contextWindow: 128000, supportsTools: true },
|
|
73
|
+
{ id: 'deepseek/deepseek-chat', name: 'DeepSeek V3 (OpenRouter)', provider: 'openrouter', contextWindow: 128000, supportsTools: true },
|
|
74
|
+
{ id: 'google/gemini-2.0-flash-001', name: 'Gemini 2.0 Flash (OpenRouter)', provider: 'openrouter', contextWindow: 1048576, supportsTools: true },
|
|
75
|
+
{ id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B (OpenRouter)', provider: 'openrouter', contextWindow: 128000, supportsTools: true },
|
|
76
|
+
// Offline
|
|
77
|
+
{ id: 'offline', name: 'Offline (No AI)', provider: 'none', contextWindow: 0, supportsTools: true }
|
|
78
|
+
];
|
|
79
|
+
export const MODEL_ALIASES = {
|
|
80
|
+
// Common Anthropic aliases
|
|
81
|
+
'claude-3-5-sonnet': 'claude-3-5-sonnet-20241022',
|
|
82
|
+
'claude-sonnet': 'claude-3-5-sonnet-20241022',
|
|
83
|
+
'sonnet': 'claude-3-5-sonnet-20241022',
|
|
84
|
+
'claude-haiku': 'claude-3-5-haiku-20241022',
|
|
85
|
+
'haiku': 'claude-3-5-haiku-20241022',
|
|
86
|
+
'claude-opus': 'claude-opus-4-6',
|
|
87
|
+
'opus': 'claude-opus-4-6',
|
|
88
|
+
// Common OpenAI aliases
|
|
89
|
+
'4o': 'gpt-4o',
|
|
90
|
+
'4o-mini': 'gpt-4o-mini',
|
|
91
|
+
// Common Gemini aliases
|
|
92
|
+
'gemini-flash': 'gemini-2.5-flash',
|
|
93
|
+
'gemini-pro': 'gemini-2.5-pro',
|
|
94
|
+
// Common OpenRouter aliases
|
|
95
|
+
'openrouter': 'openrouter/auto',
|
|
96
|
+
'auto': 'openrouter/auto',
|
|
97
|
+
// Offline
|
|
98
|
+
'none': 'offline'
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Normalizes any model ID or alias into a canonical { provider, model, displayName } structure.
|
|
102
|
+
*/
|
|
103
|
+
export function normalizeModel(input, preferredProvider) {
|
|
104
|
+
const trimmed = input.trim();
|
|
105
|
+
if (!trimmed)
|
|
106
|
+
return undefined;
|
|
107
|
+
// 1. Direct offline check
|
|
108
|
+
if (trimmed.toLowerCase() === 'none' || trimmed.toLowerCase() === 'offline') {
|
|
109
|
+
return { provider: 'none', model: 'offline', displayName: 'Offline Mode' };
|
|
110
|
+
}
|
|
111
|
+
// 2. Check alias map first
|
|
112
|
+
const aliasTarget = MODEL_ALIASES[trimmed.toLowerCase()];
|
|
113
|
+
const lookupId = aliasTarget || trimmed;
|
|
114
|
+
// 3. Match in canonical models
|
|
115
|
+
const canonical = CANONICAL_MODELS.find(m => {
|
|
116
|
+
if (m.id.toLowerCase() !== lookupId.toLowerCase())
|
|
117
|
+
return false;
|
|
118
|
+
if (m.provider === 'openrouter') {
|
|
119
|
+
return preferredProvider === 'openrouter' || lookupId.toLowerCase().startsWith('openrouter/');
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
if (canonical) {
|
|
124
|
+
return {
|
|
125
|
+
provider: canonical.provider,
|
|
126
|
+
model: canonical.id,
|
|
127
|
+
displayName: canonical.name
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// 4. Handle openrouter/ prefix explicitly
|
|
131
|
+
if (lookupId.toLowerCase().startsWith('openrouter/')) {
|
|
132
|
+
const rawModel = lookupId.slice('openrouter/'.length);
|
|
133
|
+
return {
|
|
134
|
+
provider: 'openrouter',
|
|
135
|
+
model: lookupId,
|
|
136
|
+
displayName: `OpenRouter (${rawModel || 'auto'})`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// If preferredProvider is explicitly openrouter, preserve model string as-is for OpenRouter
|
|
140
|
+
if (preferredProvider === 'openrouter') {
|
|
141
|
+
return {
|
|
142
|
+
provider: 'openrouter',
|
|
143
|
+
model: lookupId,
|
|
144
|
+
displayName: `OpenRouter (${lookupId})`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// 5. Handle provider/model syntax
|
|
148
|
+
if (lookupId.includes('/')) {
|
|
149
|
+
const slashIdx = lookupId.indexOf('/');
|
|
150
|
+
const firstPart = lookupId.slice(0, slashIdx).toLowerCase();
|
|
151
|
+
const rest = lookupId.slice(slashIdx + 1);
|
|
152
|
+
const vendorEnvMap = {
|
|
153
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
154
|
+
openai: 'OPENAI_API_KEY',
|
|
155
|
+
gemini: 'GEMINI_API_KEY',
|
|
156
|
+
google: 'GEMINI_API_KEY'
|
|
157
|
+
};
|
|
158
|
+
// If OpenRouter key is available and direct provider key is not, route to openrouter
|
|
159
|
+
const directEnv = vendorEnvMap[firstPart];
|
|
160
|
+
if (process.env.OPENROUTER_API_KEY && directEnv && !process.env[directEnv]) {
|
|
161
|
+
return {
|
|
162
|
+
provider: 'openrouter',
|
|
163
|
+
model: lookupId,
|
|
164
|
+
displayName: `OpenRouter (${lookupId})`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const knownProviders = ['anthropic', 'openai', 'gemini', 'google'];
|
|
168
|
+
if (knownProviders.includes(firstPart)) {
|
|
169
|
+
const canonicalInner = CANONICAL_MODELS.find(m => m.id.toLowerCase() === rest.toLowerCase() && m.provider === firstPart);
|
|
170
|
+
return {
|
|
171
|
+
provider: firstPart === 'google' ? 'gemini' : firstPart,
|
|
172
|
+
model: rest,
|
|
173
|
+
displayName: canonicalInner ? canonicalInner.name : `${firstPart.toUpperCase()} ${rest}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Any other vendor/model (e.g. meta-llama/..., mistralai/..., nex-agi/...)
|
|
177
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
178
|
+
return {
|
|
179
|
+
provider: 'openrouter',
|
|
180
|
+
model: lookupId,
|
|
181
|
+
displayName: `OpenRouter (${lookupId})`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
provider: firstPart,
|
|
186
|
+
model: rest,
|
|
187
|
+
displayName: `${firstPart.toUpperCase()} ${rest}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// 6. Intelligent prefix inference
|
|
191
|
+
if (lookupId.startsWith('claude-')) {
|
|
192
|
+
return { provider: 'anthropic', model: lookupId, displayName: `Claude (${lookupId})` };
|
|
193
|
+
}
|
|
194
|
+
if (lookupId.startsWith('gpt-') || lookupId.startsWith('o1') || lookupId.startsWith('o3')) {
|
|
195
|
+
return { provider: 'openai', model: lookupId, displayName: `OpenAI (${lookupId})` };
|
|
196
|
+
}
|
|
197
|
+
if (lookupId.startsWith('gemini-')) {
|
|
198
|
+
return { provider: 'gemini', model: lookupId, displayName: `Gemini (${lookupId})` };
|
|
199
|
+
}
|
|
200
|
+
// If preferredProvider is set, associate with that provider
|
|
201
|
+
if (preferredProvider) {
|
|
202
|
+
return {
|
|
203
|
+
provider: preferredProvider,
|
|
204
|
+
model: lookupId,
|
|
205
|
+
displayName: `${preferredProvider.toUpperCase()} (${lookupId})`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
3
|
+
content: string;
|
|
4
|
+
toolCallId?: string;
|
|
5
|
+
toolCalls?: ToolCall[];
|
|
6
|
+
}
|
|
7
|
+
export interface ToolCall {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
arguments: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface ToolSchema {
|
|
13
|
+
type: 'function';
|
|
14
|
+
function: {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object';
|
|
19
|
+
properties: Record<string, unknown>;
|
|
20
|
+
required: string[];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface CompletionRequest {
|
|
25
|
+
messages: Message[];
|
|
26
|
+
model: string;
|
|
27
|
+
tools?: ToolSchema[];
|
|
28
|
+
temperature?: number;
|
|
29
|
+
maxTokens?: number;
|
|
30
|
+
stream?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface CompletionResponse {
|
|
33
|
+
content: string;
|
|
34
|
+
toolCalls: ToolCall[];
|
|
35
|
+
usage?: {
|
|
36
|
+
promptTokens: number;
|
|
37
|
+
completionTokens: number;
|
|
38
|
+
totalTokens: number;
|
|
39
|
+
};
|
|
40
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
|
|
41
|
+
}
|
|
42
|
+
export interface StreamChunk {
|
|
43
|
+
type: 'text' | 'tool_call_start' | 'tool_call_args' | 'done' | 'error';
|
|
44
|
+
content?: string;
|
|
45
|
+
toolCall?: Partial<ToolCall>;
|
|
46
|
+
usage?: CompletionResponse['usage'];
|
|
47
|
+
}
|
|
48
|
+
export interface LLMProvider {
|
|
49
|
+
name: string;
|
|
50
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
51
|
+
stream(request: CompletionRequest): AsyncIterable<StreamChunk>;
|
|
52
|
+
}
|
|
53
|
+
export interface ModelInfo {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
provider: string;
|
|
57
|
+
contextWindow: number;
|
|
58
|
+
supportsTools: boolean;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|