guardlink 1.1.0 → 1.3.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/CHANGELOG.md +62 -0
- package/README.md +11 -2
- package/dist/agents/config.d.ts +17 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js +38 -4
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +5 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +4 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/launcher.d.ts +25 -8
- package/dist/agents/launcher.d.ts.map +1 -1
- package/dist/agents/launcher.js +137 -9
- package/dist/agents/launcher.js.map +1 -1
- package/dist/agents/prompts.d.ts +9 -0
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +43 -6
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/index.d.ts +44 -8
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +291 -15
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/llm.d.ts +65 -13
- package/dist/analyze/llm.d.ts.map +1 -1
- package/dist/analyze/llm.js +429 -107
- package/dist/analyze/llm.js.map +1 -1
- package/dist/analyze/prompts.d.ts +6 -2
- package/dist/analyze/prompts.d.ts.map +1 -1
- package/dist/analyze/prompts.js +230 -111
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyze/tools.d.ts +28 -0
- package/dist/analyze/tools.d.ts.map +1 -0
- package/dist/analyze/tools.js +236 -0
- package/dist/analyze/tools.js.map +1 -0
- package/dist/analyzer/index.d.ts +3 -0
- package/dist/analyzer/index.d.ts.map +1 -1
- package/dist/analyzer/index.js +3 -0
- package/dist/analyzer/index.js.map +1 -1
- package/dist/analyzer/sarif.d.ts +5 -6
- package/dist/analyzer/sarif.d.ts.map +1 -1
- package/dist/analyzer/sarif.js +5 -6
- package/dist/analyzer/sarif.js.map +1 -1
- package/dist/cli/index.d.ts +27 -16
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +524 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/data.d.ts +5 -0
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +5 -0
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/generate.d.ts +8 -5
- package/dist/dashboard/generate.d.ts.map +1 -1
- package/dist/dashboard/generate.js +206 -66
- package/dist/dashboard/generate.js.map +1 -1
- package/dist/dashboard/index.d.ts +5 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +5 -0
- package/dist/dashboard/index.js.map +1 -1
- package/dist/diff/git.d.ts +10 -7
- package/dist/diff/git.d.ts.map +1 -1
- package/dist/diff/git.js +10 -7
- package/dist/diff/git.js.map +1 -1
- package/dist/diff/index.d.ts +4 -0
- package/dist/diff/index.d.ts.map +1 -1
- package/dist/diff/index.js +4 -0
- package/dist/diff/index.js.map +1 -1
- package/dist/init/detect.d.ts +5 -0
- package/dist/init/detect.d.ts.map +1 -1
- package/dist/init/detect.js +5 -0
- package/dist/init/detect.js.map +1 -1
- package/dist/init/index.d.ts +26 -6
- package/dist/init/index.d.ts.map +1 -1
- package/dist/init/index.js +91 -11
- package/dist/init/index.js.map +1 -1
- package/dist/init/picker.d.ts.map +1 -1
- package/dist/init/picker.js +17 -6
- package/dist/init/picker.js.map +1 -1
- package/dist/init/templates.d.ts +20 -0
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +167 -36
- package/dist/init/templates.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/lookup.d.ts +5 -0
- package/dist/mcp/lookup.d.ts.map +1 -1
- package/dist/mcp/lookup.js +5 -0
- package/dist/mcp/lookup.js.map +1 -1
- package/dist/mcp/server.d.ts +16 -13
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +140 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/suggest.d.ts +8 -6
- package/dist/mcp/suggest.d.ts.map +1 -1
- package/dist/mcp/suggest.js +8 -6
- package/dist/mcp/suggest.js.map +1 -1
- package/dist/parser/clear.d.ts +36 -0
- package/dist/parser/clear.d.ts.map +1 -0
- package/dist/parser/clear.js +148 -0
- package/dist/parser/clear.js.map +1 -0
- package/dist/parser/index.d.ts +3 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +2 -1
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parse-file.d.ts +5 -2
- package/dist/parser/parse-file.d.ts.map +1 -1
- package/dist/parser/parse-file.js +29 -2
- package/dist/parser/parse-file.js.map +1 -1
- package/dist/parser/parse-line.d.ts +3 -3
- package/dist/parser/parse-line.js +3 -3
- package/dist/parser/parse-project.d.ts +7 -7
- package/dist/parser/parse-project.d.ts.map +1 -1
- package/dist/parser/parse-project.js +24 -11
- package/dist/parser/parse-project.js.map +1 -1
- package/dist/parser/validate.d.ts +12 -0
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +44 -0
- package/dist/parser/validate.js.map +1 -1
- package/dist/report/index.d.ts +3 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +3 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/report.d.ts +4 -7
- package/dist/report/report.d.ts.map +1 -1
- package/dist/report/report.js +68 -7
- package/dist/report/report.js.map +1 -1
- package/dist/review/index.d.ts +62 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/index.js +226 -0
- package/dist/review/index.js.map +1 -0
- package/dist/tui/commands.d.ts +26 -1
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +608 -101
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/config.d.ts +6 -0
- package/dist/tui/config.d.ts.map +1 -1
- package/dist/tui/config.js +6 -0
- package/dist/tui/config.js.map +1 -1
- package/dist/tui/format.d.ts +7 -0
- package/dist/tui/format.d.ts.map +1 -1
- package/dist/tui/format.js +59 -0
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/index.d.ts +8 -8
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +47 -10
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/input.d.ts +6 -0
- package/dist/tui/input.d.ts.map +1 -1
- package/dist/tui/input.js +6 -0
- package/dist/tui/input.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/analyze/llm.js
CHANGED
|
@@ -2,67 +2,63 @@
|
|
|
2
2
|
* GuardLink Threat Reports — Lightweight LLM client using raw fetch.
|
|
3
3
|
*
|
|
4
4
|
* Supports:
|
|
5
|
-
* - Anthropic Messages API (claude-sonnet-4-
|
|
6
|
-
* - OpenAI
|
|
5
|
+
* - Anthropic Messages API (claude-sonnet-4-6, claude-opus-4-6, etc.) with extended thinking + tool use
|
|
6
|
+
* - OpenAI Responses API (gpt-5.2, o3, etc.) with web search, tools, structured output
|
|
7
|
+
* - Google Gemini API (gemini-2.5-flash, gemini-3-pro, etc.) via OpenAI-compatible endpoint
|
|
8
|
+
* - OpenAI-compatible Chat Completions (DeepSeek, OpenRouter, Ollama)
|
|
9
|
+
* - DeepSeek reasoning mode (deepseek-reasoner)
|
|
7
10
|
*
|
|
8
11
|
* Zero dependencies — uses Node 20+ built-in fetch.
|
|
9
12
|
*
|
|
10
|
-
* @exposes #llm-client to #
|
|
11
|
-
* @
|
|
12
|
-
* @exposes #llm-client to #
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
15
|
-
* @
|
|
16
|
-
* @
|
|
17
|
-
* @
|
|
18
|
-
* @flows #llm-client
|
|
19
|
-
* @
|
|
13
|
+
* @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints"
|
|
14
|
+
* @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config"
|
|
15
|
+
* @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers"
|
|
16
|
+
* @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API"
|
|
17
|
+
* @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API"
|
|
18
|
+
* @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only"
|
|
19
|
+
* @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input"
|
|
20
|
+
* @flows #llm-client -> LLMProvider via fetch -- "API request output"
|
|
21
|
+
* @flows LLMProvider -> #llm-client via response -- "API response input"
|
|
22
|
+
* @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call"
|
|
23
|
+
* @handles secrets on #llm-client -- "Processes API keys for authentication"
|
|
20
24
|
*/
|
|
25
|
+
// ─── Defaults ────────────────────────────────────────────────────────
|
|
21
26
|
const DEFAULT_MODELS = {
|
|
22
|
-
anthropic: 'claude-sonnet-4-
|
|
23
|
-
openai: 'gpt-
|
|
24
|
-
|
|
27
|
+
anthropic: 'claude-sonnet-4-6',
|
|
28
|
+
openai: 'gpt-5.2',
|
|
29
|
+
google: 'gemini-2.5-flash',
|
|
30
|
+
openrouter: 'anthropic/claude-sonnet-4-6',
|
|
25
31
|
deepseek: 'deepseek-chat',
|
|
32
|
+
ollama: 'llama3.2',
|
|
26
33
|
};
|
|
27
34
|
const BASE_URLS = {
|
|
28
35
|
anthropic: 'https://api.anthropic.com',
|
|
29
36
|
openai: 'https://api.openai.com',
|
|
37
|
+
google: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
30
38
|
openrouter: 'https://openrouter.ai/api',
|
|
31
39
|
deepseek: 'https://api.deepseek.com',
|
|
40
|
+
ollama: 'http://localhost:11434',
|
|
32
41
|
};
|
|
42
|
+
// ─── Auto-detect ─────────────────────────────────────────────────────
|
|
33
43
|
/**
|
|
34
44
|
* Auto-detect provider from environment variables.
|
|
35
45
|
* Returns null if no API key found.
|
|
36
46
|
*/
|
|
37
47
|
export function autoDetectConfig() {
|
|
38
|
-
// Priority: Anthropic > OpenAI > OpenRouter > DeepSeek
|
|
39
48
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
40
|
-
return {
|
|
41
|
-
provider: 'anthropic',
|
|
42
|
-
model: DEFAULT_MODELS.anthropic,
|
|
43
|
-
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
44
|
-
};
|
|
49
|
+
return { provider: 'anthropic', model: DEFAULT_MODELS.anthropic, apiKey: process.env.ANTHROPIC_API_KEY };
|
|
45
50
|
}
|
|
46
51
|
if (process.env.OPENAI_API_KEY) {
|
|
47
|
-
return {
|
|
48
|
-
provider: 'openai',
|
|
49
|
-
model: DEFAULT_MODELS.openai,
|
|
50
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
51
|
-
};
|
|
52
|
+
return { provider: 'openai', model: DEFAULT_MODELS.openai, apiKey: process.env.OPENAI_API_KEY };
|
|
52
53
|
}
|
|
53
54
|
if (process.env.OPENROUTER_API_KEY) {
|
|
54
|
-
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
55
|
+
return { provider: 'openrouter', model: DEFAULT_MODELS.openrouter, apiKey: process.env.OPENROUTER_API_KEY };
|
|
56
|
+
}
|
|
57
|
+
if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
|
|
58
|
+
return { provider: 'google', model: DEFAULT_MODELS.google, apiKey: (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) };
|
|
59
59
|
}
|
|
60
60
|
if (process.env.DEEPSEEK_API_KEY) {
|
|
61
|
-
return {
|
|
62
|
-
provider: 'deepseek',
|
|
63
|
-
model: DEFAULT_MODELS.deepseek,
|
|
64
|
-
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
65
|
-
};
|
|
61
|
+
return { provider: 'deepseek', model: DEFAULT_MODELS.deepseek, apiKey: process.env.DEEPSEEK_API_KEY };
|
|
66
62
|
}
|
|
67
63
|
return null;
|
|
68
64
|
}
|
|
@@ -70,13 +66,13 @@ export function autoDetectConfig() {
|
|
|
70
66
|
* Build config from explicit flags + env vars.
|
|
71
67
|
*/
|
|
72
68
|
export function buildConfig(opts) {
|
|
73
|
-
// If provider specified, use it
|
|
74
69
|
if (opts.provider) {
|
|
75
70
|
const provider = opts.provider;
|
|
76
71
|
const envKeyMap = {
|
|
77
72
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
78
73
|
openai: 'OPENAI_API_KEY',
|
|
79
74
|
openrouter: 'OPENROUTER_API_KEY',
|
|
75
|
+
google: 'GOOGLE_API_KEY',
|
|
80
76
|
deepseek: 'DEEPSEEK_API_KEY',
|
|
81
77
|
};
|
|
82
78
|
const apiKey = opts.apiKey || process.env[envKeyMap[provider] || ''];
|
|
@@ -84,58 +80,120 @@ export function buildConfig(opts) {
|
|
|
84
80
|
return null;
|
|
85
81
|
return {
|
|
86
82
|
provider,
|
|
87
|
-
model: opts.model || DEFAULT_MODELS[provider] || 'gpt-
|
|
83
|
+
model: opts.model || DEFAULT_MODELS[provider] || 'gpt-5.2',
|
|
88
84
|
apiKey,
|
|
89
85
|
};
|
|
90
86
|
}
|
|
91
|
-
// Auto-detect
|
|
92
87
|
const config = autoDetectConfig();
|
|
93
88
|
if (!config)
|
|
94
89
|
return null;
|
|
95
|
-
// Override model if specified
|
|
96
90
|
if (opts.model)
|
|
97
91
|
config.model = opts.model;
|
|
98
92
|
return config;
|
|
99
93
|
}
|
|
94
|
+
// ─── Main entry point ────────────────────────────────────────────────
|
|
100
95
|
/**
|
|
101
96
|
* Send a message to the LLM and return the response.
|
|
97
|
+
* Supports streaming, tool use (agentic loop), extended thinking,
|
|
98
|
+
* web search, and structured output.
|
|
102
99
|
*/
|
|
103
100
|
export async function chatCompletion(config, systemPrompt, userMessage, onChunk) {
|
|
104
101
|
if (config.provider === 'anthropic') {
|
|
105
|
-
return
|
|
102
|
+
return callAnthropicWithTools(config, systemPrompt, userMessage, onChunk);
|
|
103
|
+
}
|
|
104
|
+
else if (config.provider === 'openai') {
|
|
105
|
+
return callOpenAIResponses(config, systemPrompt, userMessage, onChunk);
|
|
106
106
|
}
|
|
107
107
|
else {
|
|
108
|
+
// Google Gemini, DeepSeek, OpenRouter, Ollama all use OpenAI-compatible Chat Completions
|
|
108
109
|
return callOpenAICompatible(config, systemPrompt, userMessage, onChunk);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
// ─── Anthropic Messages API
|
|
112
|
-
|
|
112
|
+
// ─── Anthropic Messages API (2025) ──────────────────────────────────
|
|
113
|
+
const ANTHROPIC_API_VERSION = '2025-04-14';
|
|
114
|
+
/** Wrapper with agentic tool-call loop */
|
|
115
|
+
async function callAnthropicWithTools(config, systemPrompt, userMessage, onChunk) {
|
|
116
|
+
const maxRounds = config.maxToolRounds ?? 5;
|
|
117
|
+
let messages = [{ role: 'user', content: userMessage }];
|
|
118
|
+
const allToolCalls = [];
|
|
119
|
+
let finalResponse = null;
|
|
120
|
+
for (let round = 0; round <= maxRounds; round++) {
|
|
121
|
+
const response = await callAnthropic(config, systemPrompt, messages, round === 0 ? onChunk : undefined);
|
|
122
|
+
if (response.toolCalls?.length)
|
|
123
|
+
allToolCalls.push(...response.toolCalls);
|
|
124
|
+
if (!response.toolCalls?.length || !config.toolExecutor) {
|
|
125
|
+
finalResponse = response;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
// Add assistant response and tool results for next round
|
|
129
|
+
messages.push({ role: 'assistant', content: response._rawContent });
|
|
130
|
+
for (const tc of response.toolCalls) {
|
|
131
|
+
let resultText;
|
|
132
|
+
try {
|
|
133
|
+
resultText = await config.toolExecutor(tc.name, tc.arguments);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
resultText = `Error: ${err.message}`;
|
|
137
|
+
}
|
|
138
|
+
messages.push({
|
|
139
|
+
role: 'user',
|
|
140
|
+
content: [{ type: 'tool_result', tool_use_id: tc.id, content: resultText }],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!finalResponse)
|
|
145
|
+
throw new Error('Max tool call rounds exceeded');
|
|
146
|
+
finalResponse.toolCalls = allToolCalls.length ? allToolCalls : undefined;
|
|
147
|
+
return finalResponse;
|
|
148
|
+
}
|
|
149
|
+
async function callAnthropic(config, systemPrompt, messages, onChunk) {
|
|
113
150
|
const baseUrl = config.baseUrl || BASE_URLS.anthropic;
|
|
114
151
|
const maxTokens = config.maxTokens || 8192;
|
|
152
|
+
const headers = {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
'x-api-key': config.apiKey,
|
|
155
|
+
'anthropic-version': ANTHROPIC_API_VERSION,
|
|
156
|
+
};
|
|
157
|
+
if (config.extendedThinking) {
|
|
158
|
+
headers['anthropic-beta'] = 'interleaved-thinking-2025-05-14';
|
|
159
|
+
}
|
|
160
|
+
const body = {
|
|
161
|
+
model: config.model,
|
|
162
|
+
max_tokens: maxTokens,
|
|
163
|
+
system: systemPrompt,
|
|
164
|
+
messages,
|
|
165
|
+
};
|
|
166
|
+
if (config.extendedThinking) {
|
|
167
|
+
body.thinking = { type: 'enabled', budget_tokens: config.thinkingBudget || 10000 };
|
|
168
|
+
}
|
|
169
|
+
if (config.tools?.length) {
|
|
170
|
+
body.tools = config.tools.map(t => ({
|
|
171
|
+
name: t.name,
|
|
172
|
+
description: t.description,
|
|
173
|
+
input_schema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: t.parameters.properties,
|
|
176
|
+
required: t.parameters.required,
|
|
177
|
+
},
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
115
180
|
if (onChunk) {
|
|
116
|
-
|
|
181
|
+
body.stream = true;
|
|
117
182
|
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
118
|
-
method: 'POST',
|
|
119
|
-
headers: {
|
|
120
|
-
'Content-Type': 'application/json',
|
|
121
|
-
'x-api-key': config.apiKey,
|
|
122
|
-
'anthropic-version': '2023-06-01',
|
|
123
|
-
},
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
model: config.model,
|
|
126
|
-
max_tokens: maxTokens,
|
|
127
|
-
system: systemPrompt,
|
|
128
|
-
stream: true,
|
|
129
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
130
|
-
}),
|
|
183
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
131
184
|
});
|
|
132
185
|
if (!res.ok) {
|
|
133
186
|
const err = await res.text();
|
|
134
187
|
throw new Error(`Anthropic API error ${res.status}: ${err}`);
|
|
135
188
|
}
|
|
136
189
|
let content = '';
|
|
190
|
+
let thinking = '';
|
|
137
191
|
let inputTokens = 0;
|
|
138
192
|
let outputTokens = 0;
|
|
193
|
+
const toolCalls = [];
|
|
194
|
+
let curToolId = '';
|
|
195
|
+
let curToolName = '';
|
|
196
|
+
let curToolArgs = '';
|
|
139
197
|
const reader = res.body?.getReader();
|
|
140
198
|
if (!reader)
|
|
141
199
|
throw new Error('No response body');
|
|
@@ -155,52 +213,313 @@ async function callAnthropic(config, systemPrompt, userMessage, onChunk) {
|
|
|
155
213
|
if (data === '[DONE]')
|
|
156
214
|
continue;
|
|
157
215
|
try {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
216
|
+
const ev = JSON.parse(data);
|
|
217
|
+
if (ev.type === 'content_block_start' && ev.content_block?.type === 'tool_use') {
|
|
218
|
+
curToolId = ev.content_block.id || '';
|
|
219
|
+
curToolName = ev.content_block.name || '';
|
|
220
|
+
curToolArgs = '';
|
|
162
221
|
}
|
|
163
|
-
if (
|
|
164
|
-
|
|
222
|
+
if (ev.type === 'content_block_delta') {
|
|
223
|
+
if (ev.delta?.type === 'text_delta' && ev.delta?.text) {
|
|
224
|
+
content += ev.delta.text;
|
|
225
|
+
onChunk(ev.delta.text);
|
|
226
|
+
}
|
|
227
|
+
if (ev.delta?.type === 'thinking_delta' && ev.delta?.thinking) {
|
|
228
|
+
thinking += ev.delta.thinking;
|
|
229
|
+
}
|
|
230
|
+
if (ev.delta?.type === 'input_json_delta' && ev.delta?.partial_json) {
|
|
231
|
+
curToolArgs += ev.delta.partial_json;
|
|
232
|
+
}
|
|
165
233
|
}
|
|
166
|
-
if (
|
|
167
|
-
|
|
234
|
+
if (ev.type === 'content_block_stop' && curToolId) {
|
|
235
|
+
try {
|
|
236
|
+
toolCalls.push({ id: curToolId, name: curToolName, arguments: JSON.parse(curToolArgs || '{}') });
|
|
237
|
+
}
|
|
238
|
+
catch { /* skip */ }
|
|
239
|
+
curToolId = '';
|
|
240
|
+
curToolName = '';
|
|
241
|
+
curToolArgs = '';
|
|
168
242
|
}
|
|
243
|
+
if (ev.type === 'message_delta' && ev.usage)
|
|
244
|
+
outputTokens = ev.usage.output_tokens || 0;
|
|
245
|
+
if (ev.type === 'message_start' && ev.message?.usage)
|
|
246
|
+
inputTokens = ev.message.usage.input_tokens || 0;
|
|
169
247
|
}
|
|
170
|
-
catch { /* skip
|
|
248
|
+
catch { /* skip */ }
|
|
171
249
|
}
|
|
172
250
|
}
|
|
173
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
content, model: config.model, inputTokens, outputTokens,
|
|
253
|
+
thinking: thinking || undefined, thinkingTokens: undefined,
|
|
254
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
255
|
+
_rawContent: buildRawContent(content, thinking, toolCalls),
|
|
256
|
+
};
|
|
174
257
|
}
|
|
175
258
|
else {
|
|
176
259
|
// Non-streaming
|
|
177
260
|
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
178
|
-
method: 'POST',
|
|
179
|
-
headers: {
|
|
180
|
-
'Content-Type': 'application/json',
|
|
181
|
-
'x-api-key': config.apiKey,
|
|
182
|
-
'anthropic-version': '2023-06-01',
|
|
183
|
-
},
|
|
184
|
-
body: JSON.stringify({
|
|
185
|
-
model: config.model,
|
|
186
|
-
max_tokens: maxTokens,
|
|
187
|
-
system: systemPrompt,
|
|
188
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
189
|
-
}),
|
|
261
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
190
262
|
});
|
|
191
263
|
if (!res.ok) {
|
|
192
264
|
const err = await res.text();
|
|
193
265
|
throw new Error(`Anthropic API error ${res.status}: ${err}`);
|
|
194
266
|
}
|
|
195
267
|
const data = await res.json();
|
|
268
|
+
let content = '';
|
|
269
|
+
let thinking = '';
|
|
270
|
+
const toolCalls = [];
|
|
271
|
+
for (const block of (data.content || [])) {
|
|
272
|
+
if (block.type === 'text')
|
|
273
|
+
content += block.text;
|
|
274
|
+
if (block.type === 'thinking')
|
|
275
|
+
thinking += block.thinking;
|
|
276
|
+
if (block.type === 'tool_use') {
|
|
277
|
+
toolCalls.push({ id: block.id, name: block.name, arguments: block.input || {} });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
196
280
|
return {
|
|
197
|
-
content: data.
|
|
198
|
-
model: data.model || config.model,
|
|
281
|
+
content, model: data.model || config.model,
|
|
199
282
|
inputTokens: data.usage?.input_tokens,
|
|
200
283
|
outputTokens: data.usage?.output_tokens,
|
|
284
|
+
thinking: thinking || undefined,
|
|
285
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
286
|
+
_rawContent: data.content,
|
|
201
287
|
};
|
|
202
288
|
}
|
|
203
289
|
}
|
|
290
|
+
function buildRawContent(content, thinking, toolCalls) {
|
|
291
|
+
const blocks = [];
|
|
292
|
+
if (thinking)
|
|
293
|
+
blocks.push({ type: 'thinking', thinking });
|
|
294
|
+
if (content)
|
|
295
|
+
blocks.push({ type: 'text', text: content });
|
|
296
|
+
for (const tc of toolCalls)
|
|
297
|
+
blocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments });
|
|
298
|
+
return blocks;
|
|
299
|
+
}
|
|
300
|
+
// ─── OpenAI Responses API ────────────────────────────────────────────
|
|
301
|
+
async function callOpenAIResponses(config, systemPrompt, userMessage, onChunk) {
|
|
302
|
+
const baseUrl = config.baseUrl || BASE_URLS.openai;
|
|
303
|
+
const maxTokens = config.maxTokens || 8192;
|
|
304
|
+
const headers = {
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
307
|
+
};
|
|
308
|
+
const input = [
|
|
309
|
+
{ role: 'developer', content: systemPrompt },
|
|
310
|
+
{ role: 'user', content: userMessage },
|
|
311
|
+
];
|
|
312
|
+
const tools = [];
|
|
313
|
+
if (config.webSearch)
|
|
314
|
+
tools.push({ type: 'web_search' });
|
|
315
|
+
if (config.tools?.length) {
|
|
316
|
+
for (const t of config.tools) {
|
|
317
|
+
tools.push({
|
|
318
|
+
type: 'function', name: t.name, description: t.description,
|
|
319
|
+
parameters: t.parameters, strict: true,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const body = { model: config.model, input, max_output_tokens: maxTokens };
|
|
324
|
+
if (tools.length)
|
|
325
|
+
body.tools = tools;
|
|
326
|
+
if (config.responseFormat === 'json')
|
|
327
|
+
body.text = { format: { type: 'json_object' } };
|
|
328
|
+
if (onChunk) {
|
|
329
|
+
body.stream = true;
|
|
330
|
+
const res = await fetch(`${baseUrl}/v1/responses`, {
|
|
331
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
332
|
+
});
|
|
333
|
+
if (!res.ok) {
|
|
334
|
+
const err = await res.text();
|
|
335
|
+
// Fallback to Chat Completions if Responses API not available
|
|
336
|
+
if (res.status === 404)
|
|
337
|
+
return callOpenAICompatible(config, systemPrompt, userMessage, onChunk);
|
|
338
|
+
throw new Error(`OpenAI API error ${res.status}: ${err}`);
|
|
339
|
+
}
|
|
340
|
+
let content = '';
|
|
341
|
+
let inputTokens = 0;
|
|
342
|
+
let outputTokens = 0;
|
|
343
|
+
const toolCalls = [];
|
|
344
|
+
const reader = res.body?.getReader();
|
|
345
|
+
if (!reader)
|
|
346
|
+
throw new Error('No response body');
|
|
347
|
+
const decoder = new TextDecoder();
|
|
348
|
+
let buffer = '';
|
|
349
|
+
while (true) {
|
|
350
|
+
const { done, value } = await reader.read();
|
|
351
|
+
if (done)
|
|
352
|
+
break;
|
|
353
|
+
buffer += decoder.decode(value, { stream: true });
|
|
354
|
+
const lines = buffer.split('\n');
|
|
355
|
+
buffer = lines.pop() || '';
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
if (!line.startsWith('data: '))
|
|
358
|
+
continue;
|
|
359
|
+
const d = line.slice(6).trim();
|
|
360
|
+
if (d === '[DONE]')
|
|
361
|
+
continue;
|
|
362
|
+
try {
|
|
363
|
+
const ev = JSON.parse(d);
|
|
364
|
+
if (ev.type === 'response.output_text.delta' && ev.delta) {
|
|
365
|
+
content += ev.delta;
|
|
366
|
+
onChunk(ev.delta);
|
|
367
|
+
}
|
|
368
|
+
if (ev.type === 'response.function_call_arguments.done') {
|
|
369
|
+
try {
|
|
370
|
+
toolCalls.push({ id: ev.call_id || '', name: ev.name || '', arguments: JSON.parse(ev.arguments || '{}') });
|
|
371
|
+
}
|
|
372
|
+
catch { /* skip */ }
|
|
373
|
+
}
|
|
374
|
+
if (ev.type === 'response.completed' && ev.response?.usage) {
|
|
375
|
+
inputTokens = ev.response.usage.input_tokens || 0;
|
|
376
|
+
outputTokens = ev.response.usage.output_tokens || 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch { /* skip */ }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (toolCalls.length && config.toolExecutor) {
|
|
383
|
+
return handleOpenAIToolLoop(config, baseUrl, headers, body, content, toolCalls, inputTokens, outputTokens, onChunk);
|
|
384
|
+
}
|
|
385
|
+
return { content, model: config.model, inputTokens, outputTokens, toolCalls: toolCalls.length ? toolCalls : undefined };
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// Non-streaming
|
|
389
|
+
const res = await fetch(`${baseUrl}/v1/responses`, {
|
|
390
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
391
|
+
});
|
|
392
|
+
if (!res.ok) {
|
|
393
|
+
const err = await res.text();
|
|
394
|
+
if (res.status === 404)
|
|
395
|
+
return callOpenAICompatible(config, systemPrompt, userMessage, undefined);
|
|
396
|
+
throw new Error(`OpenAI API error ${res.status}: ${err}`);
|
|
397
|
+
}
|
|
398
|
+
const data = await res.json();
|
|
399
|
+
let content = '';
|
|
400
|
+
const toolCalls = [];
|
|
401
|
+
for (const item of (data.output || [])) {
|
|
402
|
+
if (item.type === 'message') {
|
|
403
|
+
for (const part of (item.content || [])) {
|
|
404
|
+
if (part.type === 'output_text')
|
|
405
|
+
content += part.text;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (item.type === 'function_call') {
|
|
409
|
+
try {
|
|
410
|
+
toolCalls.push({ id: item.call_id || item.id || '', name: item.name || '', arguments: JSON.parse(item.arguments || '{}') });
|
|
411
|
+
}
|
|
412
|
+
catch { /* skip */ }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!content && data.output_text)
|
|
416
|
+
content = data.output_text;
|
|
417
|
+
if (toolCalls.length && config.toolExecutor) {
|
|
418
|
+
return handleOpenAIToolLoop(config, baseUrl, headers, body, content, toolCalls, data.usage?.input_tokens, data.usage?.output_tokens, undefined);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
content, model: data.model || config.model,
|
|
422
|
+
inputTokens: data.usage?.input_tokens, outputTokens: data.usage?.output_tokens,
|
|
423
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/** Agentic tool-call loop for OpenAI Responses API */
|
|
428
|
+
async function handleOpenAIToolLoop(config, baseUrl, headers, origBody, partialContent, pending, inTok, outTok, onChunk) {
|
|
429
|
+
const maxRounds = config.maxToolRounds ?? 5;
|
|
430
|
+
const all = [...pending];
|
|
431
|
+
let content = partialContent;
|
|
432
|
+
let inputTokens = inTok;
|
|
433
|
+
let outputTokens = outTok;
|
|
434
|
+
for (let round = 0; round < maxRounds && pending.length; round++) {
|
|
435
|
+
const results = [];
|
|
436
|
+
for (const tc of pending) {
|
|
437
|
+
let r;
|
|
438
|
+
try {
|
|
439
|
+
r = await config.toolExecutor(tc.name, tc.arguments);
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
r = `Error: ${e.message}`;
|
|
443
|
+
}
|
|
444
|
+
results.push({ type: 'function_call_output', call_id: tc.id, output: r });
|
|
445
|
+
}
|
|
446
|
+
const followUp = { ...origBody, input: results, stream: !!onChunk };
|
|
447
|
+
const res = await fetch(`${baseUrl}/v1/responses`, { method: 'POST', headers, body: JSON.stringify(followUp) });
|
|
448
|
+
if (!res.ok) {
|
|
449
|
+
const err = await res.text();
|
|
450
|
+
throw new Error(`OpenAI tool follow-up error ${res.status}: ${err}`);
|
|
451
|
+
}
|
|
452
|
+
pending = [];
|
|
453
|
+
if (onChunk) {
|
|
454
|
+
const reader = res.body?.getReader();
|
|
455
|
+
if (!reader)
|
|
456
|
+
throw new Error('No response body');
|
|
457
|
+
const dec = new TextDecoder();
|
|
458
|
+
let buf = '';
|
|
459
|
+
while (true) {
|
|
460
|
+
const { done, value } = await reader.read();
|
|
461
|
+
if (done)
|
|
462
|
+
break;
|
|
463
|
+
buf += dec.decode(value, { stream: true });
|
|
464
|
+
const lines = buf.split('\n');
|
|
465
|
+
buf = lines.pop() || '';
|
|
466
|
+
for (const ln of lines) {
|
|
467
|
+
if (!ln.startsWith('data: '))
|
|
468
|
+
continue;
|
|
469
|
+
const d = ln.slice(6).trim();
|
|
470
|
+
if (d === '[DONE]')
|
|
471
|
+
continue;
|
|
472
|
+
try {
|
|
473
|
+
const ev = JSON.parse(d);
|
|
474
|
+
if (ev.type === 'response.output_text.delta' && ev.delta) {
|
|
475
|
+
content += ev.delta;
|
|
476
|
+
onChunk(ev.delta);
|
|
477
|
+
}
|
|
478
|
+
if (ev.type === 'response.function_call_arguments.done') {
|
|
479
|
+
try {
|
|
480
|
+
const tc = { id: ev.call_id || '', name: ev.name || '', arguments: JSON.parse(ev.arguments || '{}') };
|
|
481
|
+
pending.push(tc);
|
|
482
|
+
all.push(tc);
|
|
483
|
+
}
|
|
484
|
+
catch { /* skip */ }
|
|
485
|
+
}
|
|
486
|
+
if (ev.type === 'response.completed' && ev.response?.usage) {
|
|
487
|
+
inputTokens = (inputTokens || 0) + (ev.response.usage.input_tokens || 0);
|
|
488
|
+
outputTokens = (outputTokens || 0) + (ev.response.usage.output_tokens || 0);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch { /* skip */ }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const data = await res.json();
|
|
497
|
+
for (const item of (data.output || [])) {
|
|
498
|
+
if (item.type === 'message') {
|
|
499
|
+
for (const p of (item.content || [])) {
|
|
500
|
+
if (p.type === 'output_text')
|
|
501
|
+
content += p.text;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (item.type === 'function_call') {
|
|
505
|
+
try {
|
|
506
|
+
const tc = { id: item.call_id || item.id || '', name: item.name || '', arguments: JSON.parse(item.arguments || '{}') };
|
|
507
|
+
pending.push(tc);
|
|
508
|
+
all.push(tc);
|
|
509
|
+
}
|
|
510
|
+
catch { /* skip */ }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (data.output_text && !content)
|
|
514
|
+
content = data.output_text;
|
|
515
|
+
if (data.usage) {
|
|
516
|
+
inputTokens = (inputTokens || 0) + (data.usage.input_tokens || 0);
|
|
517
|
+
outputTokens = (outputTokens || 0) + (data.usage.output_tokens || 0);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { content, model: config.model, inputTokens, outputTokens, toolCalls: all.length ? all : undefined };
|
|
522
|
+
}
|
|
204
523
|
// ─── OpenAI-compatible Chat Completions ──────────────────────────────
|
|
205
524
|
async function callOpenAICompatible(config, systemPrompt, userMessage, onChunk) {
|
|
206
525
|
const baseUrl = config.baseUrl || BASE_URLS[config.provider] || BASE_URLS.openai;
|
|
@@ -209,31 +528,39 @@ async function callOpenAICompatible(config, systemPrompt, userMessage, onChunk)
|
|
|
209
528
|
'Content-Type': 'application/json',
|
|
210
529
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
211
530
|
};
|
|
212
|
-
// OpenRouter requires extra headers
|
|
213
531
|
if (config.provider === 'openrouter') {
|
|
214
532
|
headers['HTTP-Referer'] = 'https://guardlink.bugb.io';
|
|
215
533
|
headers['X-Title'] = 'GuardLink CLI';
|
|
216
534
|
}
|
|
535
|
+
const isDeepSeekReasoner = config.provider === 'deepseek' && config.model.includes('reasoner');
|
|
536
|
+
const body = {
|
|
537
|
+
model: config.model,
|
|
538
|
+
max_tokens: maxTokens,
|
|
539
|
+
messages: [
|
|
540
|
+
{ role: 'system', content: systemPrompt },
|
|
541
|
+
{ role: 'user', content: userMessage },
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
if (config.responseFormat === 'json') {
|
|
545
|
+
body.response_format = { type: 'json_object' };
|
|
546
|
+
}
|
|
547
|
+
if (config.tools?.length) {
|
|
548
|
+
body.tools = config.tools.map(t => ({
|
|
549
|
+
type: 'function',
|
|
550
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
217
553
|
if (onChunk) {
|
|
218
|
-
|
|
554
|
+
body.stream = true;
|
|
219
555
|
const res = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
220
|
-
method: 'POST',
|
|
221
|
-
headers,
|
|
222
|
-
body: JSON.stringify({
|
|
223
|
-
model: config.model,
|
|
224
|
-
max_tokens: maxTokens,
|
|
225
|
-
stream: true,
|
|
226
|
-
messages: [
|
|
227
|
-
{ role: 'system', content: systemPrompt },
|
|
228
|
-
{ role: 'user', content: userMessage },
|
|
229
|
-
],
|
|
230
|
-
}),
|
|
556
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
231
557
|
});
|
|
232
558
|
if (!res.ok) {
|
|
233
559
|
const err = await res.text();
|
|
234
560
|
throw new Error(`${config.provider} API error ${res.status}: ${err}`);
|
|
235
561
|
}
|
|
236
562
|
let content = '';
|
|
563
|
+
let reasoning = '';
|
|
237
564
|
const reader = res.body?.getReader();
|
|
238
565
|
if (!reader)
|
|
239
566
|
throw new Error('No response body');
|
|
@@ -259,36 +586,31 @@ async function callOpenAICompatible(config, systemPrompt, userMessage, onChunk)
|
|
|
259
586
|
content += delta;
|
|
260
587
|
onChunk(delta);
|
|
261
588
|
}
|
|
589
|
+
const reasoningDelta = event.choices?.[0]?.delta?.reasoning_content;
|
|
590
|
+
if (reasoningDelta)
|
|
591
|
+
reasoning += reasoningDelta;
|
|
262
592
|
}
|
|
263
593
|
catch { /* skip */ }
|
|
264
594
|
}
|
|
265
595
|
}
|
|
266
|
-
return { content, model: config.model };
|
|
596
|
+
return { content, model: config.model, thinking: reasoning || undefined };
|
|
267
597
|
}
|
|
268
598
|
else {
|
|
269
|
-
// Non-streaming
|
|
270
599
|
const res = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
271
|
-
method: 'POST',
|
|
272
|
-
headers,
|
|
273
|
-
body: JSON.stringify({
|
|
274
|
-
model: config.model,
|
|
275
|
-
max_tokens: maxTokens,
|
|
276
|
-
messages: [
|
|
277
|
-
{ role: 'system', content: systemPrompt },
|
|
278
|
-
{ role: 'user', content: userMessage },
|
|
279
|
-
],
|
|
280
|
-
}),
|
|
600
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
281
601
|
});
|
|
282
602
|
if (!res.ok) {
|
|
283
603
|
const err = await res.text();
|
|
284
604
|
throw new Error(`${config.provider} API error ${res.status}: ${err}`);
|
|
285
605
|
}
|
|
286
606
|
const data = await res.json();
|
|
607
|
+
const choice = data.choices?.[0];
|
|
287
608
|
return {
|
|
288
|
-
content:
|
|
609
|
+
content: choice?.message?.content || '',
|
|
289
610
|
model: data.model || config.model,
|
|
290
611
|
inputTokens: data.usage?.prompt_tokens,
|
|
291
612
|
outputTokens: data.usage?.completion_tokens,
|
|
613
|
+
thinking: isDeepSeekReasoner ? (choice?.message?.reasoning_content || undefined) : undefined,
|
|
292
614
|
};
|
|
293
615
|
}
|
|
294
616
|
}
|