@germanescobar/anita 0.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/README.md +353 -0
- package/dist/agent/agents.d.ts +16 -0
- package/dist/agent/agents.js +115 -0
- package/dist/agent/context-budget.d.ts +7 -0
- package/dist/agent/context-budget.js +17 -0
- package/dist/agent/context-builder.d.ts +34 -0
- package/dist/agent/context-builder.js +175 -0
- package/dist/agent/executor.d.ts +13 -0
- package/dist/agent/executor.js +65 -0
- package/dist/agent/loop.d.ts +54 -0
- package/dist/agent/loop.js +548 -0
- package/dist/agent/policies.d.ts +25 -0
- package/dist/agent/policies.js +177 -0
- package/dist/agent/session.d.ts +12 -0
- package/dist/agent/session.js +42 -0
- package/dist/attachments.d.ts +3 -0
- package/dist/attachments.js +73 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +327 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/models/anthropic.d.ts +15 -0
- package/dist/models/anthropic.js +195 -0
- package/dist/models/openai-responses.d.ts +62 -0
- package/dist/models/openai-responses.js +377 -0
- package/dist/models/openai.d.ts +32 -0
- package/dist/models/openai.js +330 -0
- package/dist/models/provider.d.ts +33 -0
- package/dist/models/provider.js +1 -0
- package/dist/models/resolve.d.ts +48 -0
- package/dist/models/resolve.js +211 -0
- package/dist/security/sensitive-content.d.ts +6 -0
- package/dist/security/sensitive-content.js +59 -0
- package/dist/skills/skills.d.ts +62 -0
- package/dist/skills/skills.js +371 -0
- package/dist/storage/event-store.d.ts +7 -0
- package/dist/storage/event-store.js +36 -0
- package/dist/storage/session-store.d.ts +11 -0
- package/dist/storage/session-store.js +64 -0
- package/dist/tools/delete-file.d.ts +2 -0
- package/dist/tools/delete-file.js +25 -0
- package/dist/tools/edit-file.d.ts +2 -0
- package/dist/tools/edit-file.js +50 -0
- package/dist/tools/read-file.d.ts +2 -0
- package/dist/tools/read-file.js +122 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.js +122 -0
- package/dist/tools/run-command.d.ts +2 -0
- package/dist/tools/run-command.js +103 -0
- package/dist/tools/write-file.d.ts +2 -0
- package/dist/tools/write-file.js +29 -0
- package/dist/types/agent.d.ts +44 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/conversation.d.ts +43 -0
- package/dist/types/conversation.js +201 -0
- package/dist/types/events.d.ts +8 -0
- package/dist/types/events.js +1 -0
- package/dist/types/messages.d.ts +39 -0
- package/dist/types/messages.js +1 -0
- package/dist/types/output.d.ts +19 -0
- package/dist/types/output.js +1 -0
- package/dist/types/stream.d.ts +55 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/tools.d.ts +28 -0
- package/dist/types/tools.js +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { conversationItemsToMessages } from "../types/conversation.js";
|
|
3
|
+
export class OpenAIProvider {
|
|
4
|
+
client;
|
|
5
|
+
model;
|
|
6
|
+
maxTokens;
|
|
7
|
+
openRouter;
|
|
8
|
+
constructor(model, options) {
|
|
9
|
+
this.client = new OpenAI({
|
|
10
|
+
apiKey: options?.apiKey ?? process.env.OPENAI_API_KEY ?? "not-needed",
|
|
11
|
+
baseURL: options?.baseURL,
|
|
12
|
+
});
|
|
13
|
+
this.model = model;
|
|
14
|
+
this.maxTokens = options?.maxTokens;
|
|
15
|
+
this.openRouter = options?.openRouter ?? false;
|
|
16
|
+
}
|
|
17
|
+
async chat(params) {
|
|
18
|
+
const messages = this.toOpenAIMessages(params.systemPrompt, conversationItemsToMessages(params.conversationItems));
|
|
19
|
+
const tools = params.tools.map((t) => this.toOpenAITool(t));
|
|
20
|
+
const request = this.withOpenRouterSessionId({
|
|
21
|
+
model: this.model,
|
|
22
|
+
messages,
|
|
23
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
24
|
+
max_tokens: this.maxTokens,
|
|
25
|
+
}, params);
|
|
26
|
+
const response = await this.client.chat.completions.create(request, {
|
|
27
|
+
signal: params.signal,
|
|
28
|
+
});
|
|
29
|
+
const choice = response.choices[0];
|
|
30
|
+
if (!choice) {
|
|
31
|
+
return { stopReason: "error", content: [{ type: "text", text: "No response from model" }] };
|
|
32
|
+
}
|
|
33
|
+
const content = this.fromOpenAIMessage(choice.message);
|
|
34
|
+
const stopReason = choice.finish_reason === "tool_calls" ? "tool_use" : "end_turn";
|
|
35
|
+
return {
|
|
36
|
+
stopReason,
|
|
37
|
+
content,
|
|
38
|
+
reasoning: this.extractReasoning(choice.message),
|
|
39
|
+
usage: response.usage
|
|
40
|
+
? this.mapUsage(response.usage)
|
|
41
|
+
: undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async *streamChat(params) {
|
|
45
|
+
const messages = this.toOpenAIMessages(params.systemPrompt, conversationItemsToMessages(params.conversationItems));
|
|
46
|
+
const tools = params.tools.map((t) => this.toOpenAITool(t));
|
|
47
|
+
const streamParams = this.withOpenRouterSessionId({
|
|
48
|
+
model: this.model,
|
|
49
|
+
messages,
|
|
50
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
51
|
+
max_tokens: this.maxTokens,
|
|
52
|
+
stream: true,
|
|
53
|
+
stream_options: { include_usage: true },
|
|
54
|
+
}, params);
|
|
55
|
+
const stream = await this.createChatCompletionStream(streamParams, params.signal);
|
|
56
|
+
let text = "";
|
|
57
|
+
let reasoning = "";
|
|
58
|
+
let stopReason = "end_turn";
|
|
59
|
+
let usage;
|
|
60
|
+
const toolCalls = new Map();
|
|
61
|
+
for await (const chunk of stream) {
|
|
62
|
+
if (chunk.usage) {
|
|
63
|
+
usage = this.mapUsage(chunk.usage);
|
|
64
|
+
}
|
|
65
|
+
const choice = chunk.choices[0];
|
|
66
|
+
if (!choice)
|
|
67
|
+
continue;
|
|
68
|
+
const delta = choice.delta;
|
|
69
|
+
if (delta.content) {
|
|
70
|
+
text += delta.content;
|
|
71
|
+
yield { type: "assistant_text_delta", text: delta.content };
|
|
72
|
+
}
|
|
73
|
+
const reasoningDelta = this.extractReasoningDelta(delta);
|
|
74
|
+
if (reasoningDelta) {
|
|
75
|
+
reasoning += reasoningDelta;
|
|
76
|
+
yield { type: "assistant_reasoning_delta", text: reasoningDelta };
|
|
77
|
+
}
|
|
78
|
+
for (const toolCall of delta.tool_calls ?? []) {
|
|
79
|
+
const existing = toolCalls.get(toolCall.index) ?? { arguments: "" };
|
|
80
|
+
if (toolCall.id)
|
|
81
|
+
existing.id = toolCall.id;
|
|
82
|
+
if (toolCall.function?.name)
|
|
83
|
+
existing.name = toolCall.function.name;
|
|
84
|
+
if (toolCall.function?.arguments) {
|
|
85
|
+
existing.arguments += toolCall.function.arguments;
|
|
86
|
+
}
|
|
87
|
+
toolCalls.set(toolCall.index, existing);
|
|
88
|
+
yield {
|
|
89
|
+
type: "tool_call_delta",
|
|
90
|
+
index: toolCall.index,
|
|
91
|
+
id: existing.id,
|
|
92
|
+
name: existing.name,
|
|
93
|
+
inputDelta: toolCall.function?.arguments,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (choice.finish_reason) {
|
|
97
|
+
stopReason = this.mapFinishReason(choice.finish_reason);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const content = [];
|
|
101
|
+
if (text) {
|
|
102
|
+
content.push({ type: "text", text });
|
|
103
|
+
}
|
|
104
|
+
for (const [, toolCall] of [...toolCalls].sort(([left], [right]) => left - right)) {
|
|
105
|
+
if (!toolCall.id || !toolCall.name)
|
|
106
|
+
continue;
|
|
107
|
+
content.push({
|
|
108
|
+
type: "tool_use",
|
|
109
|
+
id: toolCall.id,
|
|
110
|
+
name: toolCall.name,
|
|
111
|
+
input: this.parseToolArguments(toolCall.arguments),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
yield {
|
|
115
|
+
type: "response",
|
|
116
|
+
response: {
|
|
117
|
+
stopReason,
|
|
118
|
+
content,
|
|
119
|
+
reasoning: reasoning || undefined,
|
|
120
|
+
usage,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
withOpenRouterSessionId(request, params) {
|
|
125
|
+
if (!this.openRouter || !params.sessionId)
|
|
126
|
+
return request;
|
|
127
|
+
if (params.sessionId.length > 256) {
|
|
128
|
+
throw new Error("OpenRouter session_id must be at most 256 characters.");
|
|
129
|
+
}
|
|
130
|
+
return { ...request, session_id: params.sessionId };
|
|
131
|
+
}
|
|
132
|
+
mapUsage(usage) {
|
|
133
|
+
const result = {
|
|
134
|
+
inputTokens: usage.prompt_tokens,
|
|
135
|
+
outputTokens: usage.completion_tokens,
|
|
136
|
+
};
|
|
137
|
+
const cachedTokens = usage.prompt_tokens_details?.cached_tokens;
|
|
138
|
+
const cacheWriteTokens = usage.prompt_tokens_details?.cache_write_tokens;
|
|
139
|
+
if (typeof cachedTokens === "number") {
|
|
140
|
+
result.cachedTokens = cachedTokens;
|
|
141
|
+
}
|
|
142
|
+
if (typeof cacheWriteTokens === "number") {
|
|
143
|
+
result.cacheWriteTokens = cacheWriteTokens;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
async createChatCompletionStream(params, signal) {
|
|
148
|
+
try {
|
|
149
|
+
return await this.client.chat.completions.create(params, { signal });
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (!this.isUnsupportedStreamOptionsError(err)) {
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
const { stream_options: _streamOptions, ...fallbackParams } = params;
|
|
156
|
+
return this.client.chat.completions.create(fallbackParams, { signal });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
toOpenAIMessages(systemPrompt, messages) {
|
|
160
|
+
const result = [
|
|
161
|
+
{ role: "system", content: systemPrompt },
|
|
162
|
+
];
|
|
163
|
+
for (const msg of messages) {
|
|
164
|
+
if (typeof msg.content === "string") {
|
|
165
|
+
result.push({ role: msg.role, content: msg.content });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (msg.role === "assistant") {
|
|
169
|
+
const textParts = msg.content
|
|
170
|
+
.filter((b) => b.type === "text")
|
|
171
|
+
.map((b) => b.text)
|
|
172
|
+
.join("");
|
|
173
|
+
const toolCalls = msg.content
|
|
174
|
+
.filter((b) => b.type === "tool_use")
|
|
175
|
+
.map((b) => {
|
|
176
|
+
const block = b;
|
|
177
|
+
return {
|
|
178
|
+
id: block.id,
|
|
179
|
+
type: "function",
|
|
180
|
+
function: {
|
|
181
|
+
name: block.name,
|
|
182
|
+
arguments: JSON.stringify(block.input),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
// Some providers (e.g. Ollama OpenAI-compatible endpoints) reject
|
|
187
|
+
// assistant messages with null/empty content and no tool calls.
|
|
188
|
+
if (!textParts && toolCalls.length === 0) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
result.push({
|
|
192
|
+
role: "assistant",
|
|
193
|
+
content: textParts || "",
|
|
194
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// User messages may contain tool_result and attachment blocks.
|
|
199
|
+
const toolResults = msg.content.filter((b) => b.type === "tool_result");
|
|
200
|
+
const contentParts = msg.content.filter((b) => b.type === "text" || b.type === "image" || b.type === "file");
|
|
201
|
+
for (const block of toolResults) {
|
|
202
|
+
const tr = block;
|
|
203
|
+
result.push({
|
|
204
|
+
role: "tool",
|
|
205
|
+
tool_call_id: tr.toolUseId,
|
|
206
|
+
content: tr.content,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (contentParts.length > 0) {
|
|
210
|
+
result.push({
|
|
211
|
+
role: "user",
|
|
212
|
+
content: this.toOpenAIUserContent(contentParts),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
toOpenAITool(tool) {
|
|
220
|
+
return {
|
|
221
|
+
type: "function",
|
|
222
|
+
function: {
|
|
223
|
+
name: tool.name,
|
|
224
|
+
description: tool.description,
|
|
225
|
+
parameters: tool.parameters,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
toOpenAIUserContent(blocks) {
|
|
230
|
+
if (blocks.every((block) => block.type === "text")) {
|
|
231
|
+
return blocks.map((block) => block.text).join("\n");
|
|
232
|
+
}
|
|
233
|
+
return blocks.map((block) => {
|
|
234
|
+
switch (block.type) {
|
|
235
|
+
case "text":
|
|
236
|
+
return { type: "text", text: block.text };
|
|
237
|
+
case "image":
|
|
238
|
+
return {
|
|
239
|
+
type: "image_url",
|
|
240
|
+
image_url: {
|
|
241
|
+
url: this.attachmentUrl(block),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
case "file":
|
|
245
|
+
return {
|
|
246
|
+
type: "file",
|
|
247
|
+
file: {
|
|
248
|
+
filename: block.name,
|
|
249
|
+
file_data: this.attachmentUrl(block),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
attachmentUrl(block) {
|
|
256
|
+
if (block.source.type === "url")
|
|
257
|
+
return block.source.url;
|
|
258
|
+
return `data:${block.source.mediaType};base64,${block.source.data}`;
|
|
259
|
+
}
|
|
260
|
+
fromOpenAIMessage(message) {
|
|
261
|
+
const blocks = [];
|
|
262
|
+
if (message.content) {
|
|
263
|
+
blocks.push({ type: "text", text: message.content });
|
|
264
|
+
}
|
|
265
|
+
if (message.tool_calls) {
|
|
266
|
+
for (const call of message.tool_calls) {
|
|
267
|
+
blocks.push({
|
|
268
|
+
type: "tool_use",
|
|
269
|
+
id: call.id,
|
|
270
|
+
name: call.function.name,
|
|
271
|
+
input: this.parseToolArguments(call.function.arguments),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return blocks;
|
|
276
|
+
}
|
|
277
|
+
extractReasoning(message) {
|
|
278
|
+
const reasoningMessage = message;
|
|
279
|
+
const candidate = reasoningMessage.reasoning_content ?? reasoningMessage.reasoning;
|
|
280
|
+
if (candidate == null)
|
|
281
|
+
return undefined;
|
|
282
|
+
if (typeof candidate === "string")
|
|
283
|
+
return candidate;
|
|
284
|
+
try {
|
|
285
|
+
return JSON.stringify(candidate);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return String(candidate);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
extractReasoningDelta(delta) {
|
|
292
|
+
const reasoningDelta = delta;
|
|
293
|
+
const candidate = reasoningDelta.reasoning_content ?? reasoningDelta.reasoning;
|
|
294
|
+
if (candidate == null)
|
|
295
|
+
return undefined;
|
|
296
|
+
return typeof candidate === "string" ? candidate : String(candidate);
|
|
297
|
+
}
|
|
298
|
+
mapFinishReason(finishReason) {
|
|
299
|
+
switch (finishReason) {
|
|
300
|
+
case "tool_calls":
|
|
301
|
+
case "function_call":
|
|
302
|
+
return "tool_use";
|
|
303
|
+
case "length":
|
|
304
|
+
return "max_tokens";
|
|
305
|
+
case "content_filter":
|
|
306
|
+
return "error";
|
|
307
|
+
default:
|
|
308
|
+
return "end_turn";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
parseToolArguments(inputJson) {
|
|
312
|
+
if (!inputJson)
|
|
313
|
+
return {};
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(inputJson);
|
|
316
|
+
return this.isRecord(parsed) ? parsed : {};
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return {};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
isRecord(value) {
|
|
323
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
324
|
+
}
|
|
325
|
+
isUnsupportedStreamOptionsError(err) {
|
|
326
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
327
|
+
return (message.includes("stream_options") ||
|
|
328
|
+
message.includes("include_usage"));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConversationItem } from "../types/conversation.js";
|
|
2
|
+
import type { Message } from "../types/messages.js";
|
|
3
|
+
import type { ToolSchema } from "../types/tools.js";
|
|
4
|
+
import type { ModelResponse } from "../types/agent.js";
|
|
5
|
+
export interface ChatParams {
|
|
6
|
+
systemPrompt: string;
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
conversationItems: ConversationItem[];
|
|
9
|
+
messages?: Message[];
|
|
10
|
+
tools: ToolSchema[];
|
|
11
|
+
/** Aborts the in-flight model request/stream when the run is cancelled. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
export interface ModelProvider {
|
|
15
|
+
chat(params: ChatParams): Promise<ModelResponse>;
|
|
16
|
+
streamChat?(params: ChatParams): AsyncIterable<ModelStreamEvent>;
|
|
17
|
+
}
|
|
18
|
+
export type ModelStreamEvent = {
|
|
19
|
+
type: "assistant_text_delta";
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "assistant_reasoning_delta";
|
|
23
|
+
text: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: "tool_call_delta";
|
|
26
|
+
index: number;
|
|
27
|
+
id?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
inputDelta?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "response";
|
|
32
|
+
response: ModelResponse;
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ModelProvider } from "./provider.js";
|
|
2
|
+
export declare const OLLAMA_CLOUD_MODELS: readonly ["glm-5.2", "minimax-m3", "deepseek-v4-pro", "kimi-k2.7-code"];
|
|
3
|
+
export type ModelOptionGroupName = "Ollama Local" | "Ollama Cloud" | "OpenRouter";
|
|
4
|
+
export interface ModelOption {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
group: ModelOptionGroupName;
|
|
8
|
+
contextWindowTokens: number;
|
|
9
|
+
capabilities?: ModelCapabilities;
|
|
10
|
+
}
|
|
11
|
+
export interface ModelCapabilities {
|
|
12
|
+
attachments?: AttachmentCapabilities;
|
|
13
|
+
}
|
|
14
|
+
export interface AttachmentCapabilities {
|
|
15
|
+
images: boolean;
|
|
16
|
+
files: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const MODEL_OPTIONS: readonly ModelOption[];
|
|
19
|
+
export interface ModelOptionGroup {
|
|
20
|
+
group: ModelOptionGroupName;
|
|
21
|
+
options: ModelOption[];
|
|
22
|
+
}
|
|
23
|
+
export interface ModelOptionsResult {
|
|
24
|
+
options: ModelOption[];
|
|
25
|
+
ollamaDiscoveryFailed: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ResolvedModel {
|
|
28
|
+
provider: string;
|
|
29
|
+
model: string;
|
|
30
|
+
}
|
|
31
|
+
export type ProviderConfig = {
|
|
32
|
+
type: "openai-compatible";
|
|
33
|
+
provider: string;
|
|
34
|
+
model: string;
|
|
35
|
+
contextWindowTokens: number;
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
baseURL?: string;
|
|
38
|
+
maxTokens?: number;
|
|
39
|
+
openRouter?: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function parseModelString(modelString: string): ResolvedModel;
|
|
42
|
+
export declare function groupModelOptions(options?: readonly ModelOption[]): ModelOptionGroup[];
|
|
43
|
+
export declare function getModelOptions(fetchImpl?: typeof fetch): Promise<ModelOptionsResult>;
|
|
44
|
+
export declare function discoverLocalOllamaModelOptions(fetchImpl?: typeof fetch): Promise<ModelOption[] | undefined>;
|
|
45
|
+
export declare function getModelContextWindowTokens(modelString: string): number;
|
|
46
|
+
export declare function getModelCapabilities(modelString: string): ModelCapabilities;
|
|
47
|
+
export declare function resolveProviderConfig(modelString: string): ProviderConfig;
|
|
48
|
+
export declare function createProvider(modelString: string): ModelProvider;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { OpenAIProvider } from "./openai.js";
|
|
2
|
+
const OLLAMA_BASE_URL = "http://localhost:11434/v1";
|
|
3
|
+
const OLLAMA_TAGS_URL = "http://localhost:11434/api/tags";
|
|
4
|
+
const OLLAMA_CLOUD_BASE_URL = "https://ollama.com/v1";
|
|
5
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
6
|
+
const OLLAMA_DISCOVERY_TIMEOUT_MS = 1_000;
|
|
7
|
+
const OLLAMA_CLOUD_MAX_TOKENS = 8_192;
|
|
8
|
+
const UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS = 128_000;
|
|
9
|
+
const GLM_CONTEXT_WINDOW_TOKENS = 198_000;
|
|
10
|
+
const GLM_5_2_CONTEXT_WINDOW_TOKENS = 976_000;
|
|
11
|
+
const DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
12
|
+
const KIMI_K2_CONTEXT_WINDOW_TOKENS = 256_000;
|
|
13
|
+
const MINIMAX_M3_OLLAMA_CONTEXT_WINDOW_TOKENS = 512_000;
|
|
14
|
+
const MINIMAX_M3_OPENROUTER_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
15
|
+
export const OLLAMA_CLOUD_MODELS = [
|
|
16
|
+
"glm-5.2",
|
|
17
|
+
"minimax-m3",
|
|
18
|
+
"deepseek-v4-pro",
|
|
19
|
+
"kimi-k2.7-code",
|
|
20
|
+
];
|
|
21
|
+
const OLLAMA_CLOUD_CONTEXT_WINDOWS = {
|
|
22
|
+
"glm-5.2": GLM_5_2_CONTEXT_WINDOW_TOKENS,
|
|
23
|
+
"minimax-m3": MINIMAX_M3_OLLAMA_CONTEXT_WINDOW_TOKENS,
|
|
24
|
+
"deepseek-v4-pro": DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS,
|
|
25
|
+
"kimi-k2.7-code": KIMI_K2_CONTEXT_WINDOW_TOKENS,
|
|
26
|
+
};
|
|
27
|
+
export const MODEL_OPTIONS = [
|
|
28
|
+
{
|
|
29
|
+
label: "GLM 4.7 Flash (local)",
|
|
30
|
+
value: "ollama/glm-4.7-flash:latest",
|
|
31
|
+
group: "Ollama Local",
|
|
32
|
+
contextWindowTokens: GLM_CONTEXT_WINDOW_TOKENS,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "GLM 5.1 (OpenRouter)",
|
|
36
|
+
value: "openrouter/z-ai/glm-5.1",
|
|
37
|
+
group: "OpenRouter",
|
|
38
|
+
contextWindowTokens: GLM_CONTEXT_WINDOW_TOKENS,
|
|
39
|
+
capabilities: { attachments: { images: false, files: true } },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "MiniMax M3 (OpenRouter)",
|
|
43
|
+
value: "openrouter/minimax/minimax-m3",
|
|
44
|
+
group: "OpenRouter",
|
|
45
|
+
contextWindowTokens: MINIMAX_M3_OPENROUTER_CONTEXT_WINDOW_TOKENS,
|
|
46
|
+
capabilities: { attachments: { images: true, files: true } },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "DeepSeek V4 Pro (OpenRouter)",
|
|
50
|
+
value: "openrouter/deepseek/deepseek-v4-pro",
|
|
51
|
+
group: "OpenRouter",
|
|
52
|
+
contextWindowTokens: DEEPSEEK_V4_PRO_CONTEXT_WINDOW_TOKENS,
|
|
53
|
+
capabilities: { attachments: { images: false, files: true } },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Kimi K2.6 (OpenRouter)",
|
|
57
|
+
value: "openrouter/moonshotai/kimi-k2.6",
|
|
58
|
+
group: "OpenRouter",
|
|
59
|
+
contextWindowTokens: KIMI_K2_CONTEXT_WINDOW_TOKENS,
|
|
60
|
+
capabilities: { attachments: { images: true, files: true } },
|
|
61
|
+
},
|
|
62
|
+
...OLLAMA_CLOUD_MODELS.map((model) => ({
|
|
63
|
+
label: `${model} (cloud)`,
|
|
64
|
+
value: `ollama-cloud/${model}`,
|
|
65
|
+
group: "Ollama Cloud",
|
|
66
|
+
contextWindowTokens: OLLAMA_CLOUD_CONTEXT_WINDOWS[model],
|
|
67
|
+
...(model === "minimax-m3"
|
|
68
|
+
? { capabilities: { attachments: { images: true, files: true } } }
|
|
69
|
+
: {}),
|
|
70
|
+
...(model === "kimi-k2.7-code"
|
|
71
|
+
? { capabilities: { attachments: { images: true, files: false } } }
|
|
72
|
+
: {}),
|
|
73
|
+
})),
|
|
74
|
+
];
|
|
75
|
+
export function parseModelString(modelString) {
|
|
76
|
+
const slashIndex = modelString.indexOf("/");
|
|
77
|
+
if (slashIndex === -1) {
|
|
78
|
+
throw new Error(`Invalid model format: "${modelString}". Expected "provider/model" (e.g., "ollama/glm-4.7-flash:latest")`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
provider: modelString.slice(0, slashIndex),
|
|
82
|
+
model: modelString.slice(slashIndex + 1),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function groupModelOptions(options = MODEL_OPTIONS) {
|
|
86
|
+
const groups = [];
|
|
87
|
+
for (const option of options) {
|
|
88
|
+
let group = groups.find((item) => item.group === option.group);
|
|
89
|
+
if (!group) {
|
|
90
|
+
group = { group: option.group, options: [] };
|
|
91
|
+
groups.push(group);
|
|
92
|
+
}
|
|
93
|
+
group.options.push(option);
|
|
94
|
+
}
|
|
95
|
+
return groups;
|
|
96
|
+
}
|
|
97
|
+
export async function getModelOptions(fetchImpl = fetch) {
|
|
98
|
+
const discoveredOllamaOptions = await discoverLocalOllamaModelOptions(fetchImpl);
|
|
99
|
+
const staticValues = new Set(MODEL_OPTIONS.map((option) => option.value));
|
|
100
|
+
const dynamicOptions = (discoveredOllamaOptions ?? []).filter((option) => !staticValues.has(option.value));
|
|
101
|
+
return {
|
|
102
|
+
options: [...MODEL_OPTIONS, ...dynamicOptions],
|
|
103
|
+
ollamaDiscoveryFailed: discoveredOllamaOptions === undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export async function discoverLocalOllamaModelOptions(fetchImpl = fetch) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetchImpl(OLLAMA_TAGS_URL, {
|
|
109
|
+
signal: AbortSignal.timeout(OLLAMA_DISCOVERY_TIMEOUT_MS),
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
return undefined;
|
|
113
|
+
const payload = await response.json();
|
|
114
|
+
return parseOllamaModelNames(payload).map((name) => ({
|
|
115
|
+
label: `${name} (local)`,
|
|
116
|
+
value: `ollama/${name}`,
|
|
117
|
+
group: "Ollama Local",
|
|
118
|
+
contextWindowTokens: UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function parseOllamaModelNames(payload) {
|
|
126
|
+
if (!isRecord(payload) || !Array.isArray(payload.models))
|
|
127
|
+
return [];
|
|
128
|
+
return payload.models
|
|
129
|
+
.map((model) => {
|
|
130
|
+
if (!isRecord(model))
|
|
131
|
+
return undefined;
|
|
132
|
+
const name = model.name ?? model.model;
|
|
133
|
+
return typeof name === "string" && name.length > 0 ? name : undefined;
|
|
134
|
+
})
|
|
135
|
+
.filter((name) => name !== undefined)
|
|
136
|
+
.sort((left, right) => left.localeCompare(right));
|
|
137
|
+
}
|
|
138
|
+
function isRecord(value) {
|
|
139
|
+
return typeof value === "object" && value !== null;
|
|
140
|
+
}
|
|
141
|
+
function assertSupportedOllamaCloudModel(model) {
|
|
142
|
+
if (OLLAMA_CLOUD_MODELS.includes(model))
|
|
143
|
+
return;
|
|
144
|
+
throw new Error(`Unsupported Ollama Cloud model: "${model}". Supported: ${OLLAMA_CLOUD_MODELS.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
export function getModelContextWindowTokens(modelString) {
|
|
147
|
+
return (MODEL_OPTIONS.find((option) => option.value === modelString)
|
|
148
|
+
?.contextWindowTokens ?? UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS);
|
|
149
|
+
}
|
|
150
|
+
export function getModelCapabilities(modelString) {
|
|
151
|
+
const option = MODEL_OPTIONS.find((item) => item.value === modelString);
|
|
152
|
+
if (option?.capabilities)
|
|
153
|
+
return option.capabilities;
|
|
154
|
+
const { provider } = parseModelString(modelString);
|
|
155
|
+
if (provider === "openrouter") {
|
|
156
|
+
return { attachments: { images: false, files: true } };
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
export function resolveProviderConfig(modelString) {
|
|
161
|
+
const { provider, model } = parseModelString(modelString);
|
|
162
|
+
const contextWindowTokens = getModelContextWindowTokens(modelString);
|
|
163
|
+
switch (provider) {
|
|
164
|
+
case "ollama":
|
|
165
|
+
return {
|
|
166
|
+
type: "openai-compatible",
|
|
167
|
+
provider,
|
|
168
|
+
model,
|
|
169
|
+
contextWindowTokens,
|
|
170
|
+
baseURL: OLLAMA_BASE_URL,
|
|
171
|
+
};
|
|
172
|
+
case "ollama-cloud":
|
|
173
|
+
assertSupportedOllamaCloudModel(model);
|
|
174
|
+
if (!process.env.OLLAMA_API_KEY) {
|
|
175
|
+
throw new Error("OLLAMA_API_KEY is required for ollama-cloud models. Set OLLAMA_API_KEY and try again.");
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
type: "openai-compatible",
|
|
179
|
+
provider,
|
|
180
|
+
model,
|
|
181
|
+
contextWindowTokens,
|
|
182
|
+
apiKey: process.env.OLLAMA_API_KEY,
|
|
183
|
+
baseURL: OLLAMA_CLOUD_BASE_URL,
|
|
184
|
+
maxTokens: OLLAMA_CLOUD_MAX_TOKENS,
|
|
185
|
+
};
|
|
186
|
+
case "openrouter":
|
|
187
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
188
|
+
throw new Error("OPENROUTER_API_KEY is required for openrouter models. Set OPENROUTER_API_KEY and try again.");
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
type: "openai-compatible",
|
|
192
|
+
provider,
|
|
193
|
+
model,
|
|
194
|
+
contextWindowTokens,
|
|
195
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
196
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
197
|
+
openRouter: true,
|
|
198
|
+
};
|
|
199
|
+
default:
|
|
200
|
+
throw new Error(`Unknown provider: "${provider}". Supported: ollama, ollama-cloud, openrouter`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export function createProvider(modelString) {
|
|
204
|
+
const config = resolveProviderConfig(modelString);
|
|
205
|
+
return new OpenAIProvider(config.model, {
|
|
206
|
+
apiKey: config.apiKey,
|
|
207
|
+
baseURL: config.baseURL,
|
|
208
|
+
maxTokens: config.maxTokens,
|
|
209
|
+
openRouter: config.openRouter,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface SensitiveContentFinding {
|
|
2
|
+
label: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function detectSensitiveContent(content: string): SensitiveContentFinding[];
|
|
5
|
+
export declare function containsSensitiveContent(content: string): boolean;
|
|
6
|
+
export declare function redactSensitiveContent(content: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const SENSITIVE_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
label: "secret assignment",
|
|
4
|
+
pattern: /\b[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY)[A-Z0-9_]*\s*[:=]\s*["']?[^\s"']{8,}["']?/gi,
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
label: "private key",
|
|
8
|
+
pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
label: "GitHub token",
|
|
12
|
+
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,255}\b/g,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: "GitHub fine-grained token",
|
|
16
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{20,255}\b/g,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "OpenAI API key",
|
|
20
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,255}\b/g,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: "Anthropic API key",
|
|
24
|
+
pattern: /\bsk-ant-[A-Za-z0-9_-]{20,255}\b/g,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "AWS access key",
|
|
28
|
+
pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Slack token",
|
|
32
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,255}\b/g,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "JWT",
|
|
36
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
export function detectSensitiveContent(content) {
|
|
40
|
+
const labels = new Set();
|
|
41
|
+
for (const sensitivePattern of SENSITIVE_PATTERNS) {
|
|
42
|
+
sensitivePattern.pattern.lastIndex = 0;
|
|
43
|
+
if (sensitivePattern.pattern.test(content)) {
|
|
44
|
+
labels.add(sensitivePattern.label);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...labels].map((label) => ({ label }));
|
|
48
|
+
}
|
|
49
|
+
export function containsSensitiveContent(content) {
|
|
50
|
+
return detectSensitiveContent(content).length > 0;
|
|
51
|
+
}
|
|
52
|
+
export function redactSensitiveContent(content) {
|
|
53
|
+
let redacted = content;
|
|
54
|
+
for (const sensitivePattern of SENSITIVE_PATTERNS) {
|
|
55
|
+
sensitivePattern.pattern.lastIndex = 0;
|
|
56
|
+
redacted = redacted.replace(sensitivePattern.pattern, `[REDACTED: ${sensitivePattern.label}]`);
|
|
57
|
+
}
|
|
58
|
+
return redacted;
|
|
59
|
+
}
|