homarus 0.1.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/agent-manager.d.ts +35 -0
  4. package/dist/agent-manager.js +127 -0
  5. package/dist/agent-worker.d.ts +2 -0
  6. package/dist/agent-worker.js +141 -0
  7. package/dist/agent.d.ts +33 -0
  8. package/dist/agent.js +197 -0
  9. package/dist/browser-manager.d.ts +33 -0
  10. package/dist/browser-manager.js +170 -0
  11. package/dist/channel-adapter.d.ts +29 -0
  12. package/dist/channel-adapter.js +94 -0
  13. package/dist/channel-manager.d.ts +17 -0
  14. package/dist/channel-manager.js +84 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.js +212 -0
  17. package/dist/config.d.ts +21 -0
  18. package/dist/config.js +185 -0
  19. package/dist/embedding-provider.d.ts +35 -0
  20. package/dist/embedding-provider.js +103 -0
  21. package/dist/event-bus.d.ts +18 -0
  22. package/dist/event-bus.js +46 -0
  23. package/dist/event-queue.d.ts +18 -0
  24. package/dist/event-queue.js +77 -0
  25. package/dist/execution-strategy.d.ts +26 -0
  26. package/dist/execution-strategy.js +20 -0
  27. package/dist/homarus.d.ts +36 -0
  28. package/dist/homarus.js +308 -0
  29. package/dist/http-api.d.ts +16 -0
  30. package/dist/http-api.js +82 -0
  31. package/dist/identity-manager.d.ts +28 -0
  32. package/dist/identity-manager.js +123 -0
  33. package/dist/memory-index.d.ts +52 -0
  34. package/dist/memory-index.js +286 -0
  35. package/dist/model-provider.d.ts +33 -0
  36. package/dist/model-provider.js +255 -0
  37. package/dist/model-router.d.ts +32 -0
  38. package/dist/model-router.js +148 -0
  39. package/dist/setup-wizard.d.ts +28 -0
  40. package/dist/setup-wizard.js +240 -0
  41. package/dist/skill-manager.d.ts +26 -0
  42. package/dist/skill-manager.js +171 -0
  43. package/dist/skill-transport.d.ts +51 -0
  44. package/dist/skill-transport.js +116 -0
  45. package/dist/skill.d.ts +22 -0
  46. package/dist/skill.js +118 -0
  47. package/dist/subprocess-strategy.d.ts +54 -0
  48. package/dist/subprocess-strategy.js +106 -0
  49. package/dist/telegram-adapter.d.ts +34 -0
  50. package/dist/telegram-adapter.js +165 -0
  51. package/dist/timer-service.d.ts +30 -0
  52. package/dist/timer-service.js +142 -0
  53. package/dist/tool-registry.d.ts +29 -0
  54. package/dist/tool-registry.js +100 -0
  55. package/dist/tools/bash.d.ts +3 -0
  56. package/dist/tools/bash.js +48 -0
  57. package/dist/tools/browser.d.ts +4 -0
  58. package/dist/tools/browser.js +47 -0
  59. package/dist/tools/edit.d.ts +3 -0
  60. package/dist/tools/edit.js +48 -0
  61. package/dist/tools/git.d.ts +3 -0
  62. package/dist/tools/git.js +109 -0
  63. package/dist/tools/glob.d.ts +3 -0
  64. package/dist/tools/glob.js +86 -0
  65. package/dist/tools/grep.d.ts +3 -0
  66. package/dist/tools/grep.js +169 -0
  67. package/dist/tools/index.d.ts +6 -0
  68. package/dist/tools/index.js +46 -0
  69. package/dist/tools/lsp.d.ts +3 -0
  70. package/dist/tools/lsp.js +216 -0
  71. package/dist/tools/memory.d.ts +4 -0
  72. package/dist/tools/memory.js +64 -0
  73. package/dist/tools/read.d.ts +3 -0
  74. package/dist/tools/read.js +49 -0
  75. package/dist/tools/web-fetch.d.ts +3 -0
  76. package/dist/tools/web-fetch.js +51 -0
  77. package/dist/tools/web-search.d.ts +3 -0
  78. package/dist/tools/web-search.js +73 -0
  79. package/dist/tools/write.d.ts +3 -0
  80. package/dist/tools/write.js +31 -0
  81. package/dist/types.d.ts +240 -0
  82. package/dist/types.js +14 -0
  83. package/package.json +69 -0
@@ -0,0 +1,286 @@
1
+ // CRC: crc-MemoryIndex.md | Seq: seq-agent-execution.md
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "node:fs";
3
+ import { join, dirname, extname } from "node:path";
4
+ import { watch } from "node:fs";
5
+ export class MemoryIndex {
6
+ db = null; // better-sqlite3 Database
7
+ embeddingProvider = null;
8
+ chunkSize;
9
+ chunkOverlap;
10
+ vectorWeight;
11
+ ftsWeight;
12
+ indexedPaths = [];
13
+ watcher = null;
14
+ logger;
15
+ initialized = false;
16
+ constructor(logger, options) {
17
+ this.logger = logger;
18
+ this.chunkSize = options?.chunkSize ?? 400;
19
+ this.chunkOverlap = options?.chunkOverlap ?? 80;
20
+ this.vectorWeight = options?.vectorWeight ?? 0.7;
21
+ this.ftsWeight = options?.ftsWeight ?? 0.3;
22
+ }
23
+ setEmbeddingProvider(provider) {
24
+ this.embeddingProvider = provider;
25
+ }
26
+ // CRC: crc-MemoryIndex.md — initialize()
27
+ async initialize(dbPath) {
28
+ // Dynamic import to avoid hard dependency when memory isn't used
29
+ const Database = (await import("better-sqlite3")).default;
30
+ this.db = new Database(dbPath);
31
+ const db = this.db;
32
+ db.pragma("journal_mode = WAL");
33
+ // Load sqlite-vec extension if available
34
+ try {
35
+ const sqliteVec = await import("sqlite-vec");
36
+ sqliteVec.load(db);
37
+ }
38
+ catch {
39
+ this.logger.warn("sqlite-vec not available, vector search disabled");
40
+ }
41
+ db.exec(`
42
+ CREATE TABLE IF NOT EXISTS chunks (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ path TEXT NOT NULL,
45
+ chunk_index INTEGER NOT NULL,
46
+ content TEXT NOT NULL,
47
+ updated_at INTEGER NOT NULL,
48
+ UNIQUE(path, chunk_index)
49
+ );
50
+ CREATE INDEX IF NOT EXISTS idx_chunks_path ON chunks(path);
51
+ `);
52
+ // FTS5 virtual table
53
+ db.exec(`
54
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
55
+ content, content='chunks', content_rowid='id'
56
+ );
57
+ `);
58
+ // Triggers to keep FTS in sync
59
+ db.exec(`
60
+ CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
61
+ INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);
62
+ END;
63
+ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
64
+ INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES('delete', old.id, old.content);
65
+ END;
66
+ CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
67
+ INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES('delete', old.id, old.content);
68
+ INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);
69
+ END;
70
+ `);
71
+ // Vector table (only if sqlite-vec loaded)
72
+ try {
73
+ const dims = this.embeddingProvider?.dimensions() ?? 768;
74
+ db.exec(`
75
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
76
+ chunk_id INTEGER PRIMARY KEY,
77
+ embedding float[${dims}]
78
+ );
79
+ `);
80
+ }
81
+ catch {
82
+ this.logger.debug("Vector table creation skipped");
83
+ }
84
+ this.initialized = true;
85
+ this.logger.info("Memory index initialized", { dbPath });
86
+ }
87
+ // CRC: crc-MemoryIndex.md — indexFile()
88
+ async indexFile(path) {
89
+ if (!this.initialized || !this.db)
90
+ return;
91
+ if (!existsSync(path))
92
+ return;
93
+ const content = readFileSync(path, "utf-8");
94
+ const chunks = this.chunkContent(content, path);
95
+ const db = this.db;
96
+ const now = Date.now();
97
+ // Remove old chunks for this file
98
+ db.prepare("DELETE FROM chunks WHERE path = ?").run(path);
99
+ const insert = db.prepare("INSERT INTO chunks (path, chunk_index, content, updated_at) VALUES (?, ?, ?, ?)");
100
+ const insertMany = db.transaction((items) => {
101
+ for (const chunk of items) {
102
+ insert.run(chunk.path, chunk.index, chunk.content, now);
103
+ }
104
+ });
105
+ insertMany(chunks);
106
+ // Generate and store embeddings
107
+ if (this.embeddingProvider) {
108
+ try {
109
+ const texts = chunks.map((c) => c.content);
110
+ const embeddings = await this.embeddingProvider.embedBatch(texts);
111
+ const rows = db.prepare("SELECT id FROM chunks WHERE path = ? ORDER BY chunk_index").all(path);
112
+ const insertVec = db.prepare("INSERT OR REPLACE INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)");
113
+ const insertVecs = db.transaction(() => {
114
+ for (let i = 0; i < rows.length && i < embeddings.length; i++) {
115
+ insertVec.run(rows[i].id, new Float32Array(embeddings[i]));
116
+ }
117
+ });
118
+ insertVecs();
119
+ }
120
+ catch (err) {
121
+ this.logger.warn("Failed to generate embeddings", { path, error: String(err) });
122
+ }
123
+ }
124
+ this.logger.debug("Indexed file", { path, chunks: chunks.length });
125
+ }
126
+ // CRC: crc-MemoryIndex.md — indexDirectory()
127
+ async indexDirectory(dirPath) {
128
+ if (!existsSync(dirPath))
129
+ return;
130
+ const files = this.findMarkdownFiles(dirPath);
131
+ for (const file of files) {
132
+ await this.indexFile(file);
133
+ }
134
+ if (!this.indexedPaths.includes(dirPath)) {
135
+ this.indexedPaths.push(dirPath);
136
+ }
137
+ this.logger.info("Indexed directory", { path: dirPath, files: files.length });
138
+ }
139
+ // CRC: crc-MemoryIndex.md — search()
140
+ async search(query, options) {
141
+ if (!this.initialized || !this.db)
142
+ return [];
143
+ const db = this.db;
144
+ const limit = options?.limit ?? 10;
145
+ const minScore = options?.minScore ?? 0.3;
146
+ const results = new Map();
147
+ // FTS search
148
+ try {
149
+ const ftsResults = db.prepare(`
150
+ SELECT c.id, c.path, c.content, c.chunk_index,
151
+ bm25(chunks_fts) AS rank
152
+ FROM chunks_fts f
153
+ JOIN chunks c ON c.id = f.rowid
154
+ WHERE chunks_fts MATCH ?
155
+ ORDER BY rank
156
+ LIMIT ?
157
+ `).all(query, limit * 2);
158
+ // Normalize FTS scores to 0-1 range
159
+ const maxRank = ftsResults.length > 0 ? Math.max(...ftsResults.map((r) => Math.abs(r.rank))) : 1;
160
+ for (const row of ftsResults) {
161
+ const normalizedScore = maxRank > 0 ? Math.abs(row.rank) / maxRank : 0;
162
+ results.set(row.id, {
163
+ path: row.path,
164
+ content: row.content,
165
+ score: normalizedScore * this.ftsWeight,
166
+ chunkIndex: row.chunk_index,
167
+ });
168
+ }
169
+ }
170
+ catch {
171
+ // FTS query may fail on certain inputs
172
+ }
173
+ // Vector search
174
+ if (this.embeddingProvider) {
175
+ try {
176
+ const queryEmb = await this.embeddingProvider.embed(query);
177
+ const vecResults = db.prepare(`
178
+ SELECT chunk_id, distance
179
+ FROM chunks_vec
180
+ WHERE embedding MATCH ?
181
+ ORDER BY distance
182
+ LIMIT ?
183
+ `).all(new Float32Array(queryEmb), limit * 2);
184
+ for (const row of vecResults) {
185
+ const similarity = 1 - row.distance; // cosine distance → similarity
186
+ const existing = results.get(row.chunk_id);
187
+ if (existing) {
188
+ existing.score += similarity * this.vectorWeight;
189
+ }
190
+ else {
191
+ const chunk = db.prepare("SELECT path, content, chunk_index FROM chunks WHERE id = ?")
192
+ .get(row.chunk_id);
193
+ if (chunk) {
194
+ results.set(row.chunk_id, {
195
+ path: chunk.path,
196
+ content: chunk.content,
197
+ score: similarity * this.vectorWeight,
198
+ chunkIndex: chunk.chunk_index,
199
+ });
200
+ }
201
+ }
202
+ }
203
+ }
204
+ catch (err) {
205
+ this.logger.debug("Vector search failed", { error: String(err) });
206
+ }
207
+ }
208
+ return [...results.values()]
209
+ .filter((r) => r.score >= minScore)
210
+ .sort((a, b) => b.score - a.score)
211
+ .slice(0, limit);
212
+ }
213
+ // CRC: crc-MemoryIndex.md — get()
214
+ get(path) {
215
+ if (!existsSync(path))
216
+ return undefined;
217
+ return readFileSync(path, "utf-8");
218
+ }
219
+ // CRC: crc-MemoryIndex.md — store()
220
+ async store(content, path) {
221
+ const dir = dirname(path);
222
+ if (!existsSync(dir))
223
+ mkdirSync(dir, { recursive: true });
224
+ writeFileSync(path, content);
225
+ await this.indexFile(path);
226
+ }
227
+ // CRC: crc-MemoryIndex.md — startWatching()
228
+ startWatching() {
229
+ if (this.watcher)
230
+ return;
231
+ for (const dir of this.indexedPaths) {
232
+ this.watcher = watch(dir, { recursive: true }, (_eventType, filename) => {
233
+ if (!filename || !filename.endsWith(".md"))
234
+ return;
235
+ const fullPath = join(dir, filename);
236
+ this.indexFile(fullPath).catch((err) => {
237
+ this.logger.warn("Reindex failed", { path: fullPath, error: String(err) });
238
+ });
239
+ });
240
+ }
241
+ }
242
+ // CRC: crc-MemoryIndex.md — stopWatching()
243
+ stopWatching() {
244
+ this.watcher?.close();
245
+ this.watcher = null;
246
+ }
247
+ // CRC: crc-MemoryIndex.md — getStats()
248
+ getStats() {
249
+ if (!this.initialized || !this.db) {
250
+ return { fileCount: 0, chunkCount: 0, indexedPaths: this.indexedPaths };
251
+ }
252
+ const db = this.db;
253
+ const fileCount = db.prepare("SELECT COUNT(DISTINCT path) AS c FROM chunks").get().c;
254
+ const chunkCount = db.prepare("SELECT COUNT(*) AS c FROM chunks").get().c;
255
+ return { fileCount, chunkCount, indexedPaths: this.indexedPaths };
256
+ }
257
+ chunkContent(content, path) {
258
+ const words = content.split(/\s+/);
259
+ const chunks = [];
260
+ let i = 0;
261
+ let index = 0;
262
+ while (i < words.length) {
263
+ const end = Math.min(i + this.chunkSize, words.length);
264
+ const chunkText = words.slice(i, end).join(" ");
265
+ chunks.push({ path, index, content: chunkText });
266
+ i += this.chunkSize - this.chunkOverlap;
267
+ index++;
268
+ }
269
+ return chunks;
270
+ }
271
+ findMarkdownFiles(dir) {
272
+ const files = [];
273
+ const entries = readdirSync(dir, { withFileTypes: true });
274
+ for (const entry of entries) {
275
+ const fullPath = join(dir, entry.name);
276
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
277
+ files.push(...this.findMarkdownFiles(fullPath));
278
+ }
279
+ else if (entry.isFile() && extname(entry.name) === ".md") {
280
+ files.push(fullPath);
281
+ }
282
+ }
283
+ return files;
284
+ }
285
+ }
286
+ //# sourceMappingURL=memory-index.js.map
@@ -0,0 +1,33 @@
1
+ import type { ChatRequest, ChatChunk, ModelInfo, AuthProfile, Logger } from "./types.js";
2
+ export declare abstract class ModelProvider {
3
+ readonly id: string;
4
+ protected baseUrl: string;
5
+ protected activeProfile: AuthProfile;
6
+ protected logger: Logger;
7
+ constructor(id: string, baseUrl: string, profile: AuthProfile, logger: Logger);
8
+ abstract chat(request: ChatRequest): AsyncIterable<ChatChunk>;
9
+ abstract listModels(): Promise<ModelInfo[]>;
10
+ abstract supportsTools(): boolean;
11
+ abstract supportsStreaming(): boolean;
12
+ setProfile(profile: AuthProfile): void;
13
+ validateAuth(): Promise<boolean>;
14
+ }
15
+ export declare class OpenAICompatibleProvider extends ModelProvider {
16
+ constructor(id: string, baseUrl: string, profile: AuthProfile, logger: Logger);
17
+ chat(request: ChatRequest): AsyncIterable<ChatChunk>;
18
+ listModels(): Promise<ModelInfo[]>;
19
+ supportsTools(): boolean;
20
+ supportsStreaming(): boolean;
21
+ private stripProviderPrefix;
22
+ private buildRequestBody;
23
+ private parseChunk;
24
+ }
25
+ export declare class AnthropicProvider extends ModelProvider {
26
+ constructor(profile: AuthProfile, logger: Logger);
27
+ chat(request: ChatRequest): AsyncIterable<ChatChunk>;
28
+ listModels(): Promise<ModelInfo[]>;
29
+ supportsTools(): boolean;
30
+ supportsStreaming(): boolean;
31
+ private parseAnthropicEvent;
32
+ }
33
+ //# sourceMappingURL=model-provider.d.ts.map
@@ -0,0 +1,255 @@
1
+ export class ModelProvider {
2
+ id;
3
+ baseUrl;
4
+ activeProfile;
5
+ logger;
6
+ constructor(id, baseUrl, profile, logger) {
7
+ this.id = id;
8
+ this.baseUrl = baseUrl;
9
+ this.activeProfile = profile;
10
+ this.logger = logger;
11
+ }
12
+ setProfile(profile) {
13
+ this.activeProfile = profile;
14
+ }
15
+ async validateAuth() {
16
+ try {
17
+ await this.listModels();
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ }
25
+ // OpenAI-compatible provider (works for OpenAI, OpenRouter, Ollama, and any compatible endpoint)
26
+ export class OpenAICompatibleProvider extends ModelProvider {
27
+ constructor(id, baseUrl, profile, logger) {
28
+ super(id, baseUrl, profile, logger);
29
+ }
30
+ async *chat(request) {
31
+ const body = this.buildRequestBody(request);
32
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ Authorization: `Bearer ${this.activeProfile.apiKey}`,
37
+ },
38
+ body: JSON.stringify({ ...body, stream: true }),
39
+ });
40
+ if (!response.ok) {
41
+ const errorText = await response.text();
42
+ throw new Error(`${this.id} API error ${response.status}: ${errorText}`);
43
+ }
44
+ const reader = response.body?.getReader();
45
+ if (!reader)
46
+ throw new Error("No response body");
47
+ const decoder = new TextDecoder();
48
+ let buffer = "";
49
+ while (true) {
50
+ const { done, value } = await reader.read();
51
+ if (done)
52
+ break;
53
+ buffer += decoder.decode(value, { stream: true });
54
+ const lines = buffer.split("\n");
55
+ buffer = lines.pop() ?? "";
56
+ for (const line of lines) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed.startsWith("data: "))
59
+ continue;
60
+ const data = trimmed.slice(6);
61
+ if (data === "[DONE]")
62
+ return;
63
+ try {
64
+ const parsed = JSON.parse(data);
65
+ const chunk = this.parseChunk(parsed);
66
+ if (chunk)
67
+ yield chunk;
68
+ }
69
+ catch {
70
+ // skip unparseable chunks
71
+ }
72
+ }
73
+ }
74
+ }
75
+ async listModels() {
76
+ const response = await fetch(`${this.baseUrl}/models`, {
77
+ headers: { Authorization: `Bearer ${this.activeProfile.apiKey}` },
78
+ });
79
+ if (!response.ok)
80
+ throw new Error(`Failed to list models: ${response.status}`);
81
+ const data = (await response.json());
82
+ return data.data.map((m) => ({ id: m.id, name: m.id }));
83
+ }
84
+ supportsTools() {
85
+ return true;
86
+ }
87
+ supportsStreaming() {
88
+ return true;
89
+ }
90
+ stripProviderPrefix(model) {
91
+ // "openrouter/anthropic/claude-sonnet-4-5" → "anthropic/claude-sonnet-4-5"
92
+ if (model.startsWith(this.id + "/")) {
93
+ return model.substring(this.id.length + 1);
94
+ }
95
+ return model;
96
+ }
97
+ buildRequestBody(request) {
98
+ const body = {
99
+ model: this.stripProviderPrefix(request.model),
100
+ messages: request.messages.map((m) => {
101
+ const msg = { role: m.role, content: m.content };
102
+ if (m.toolCallId)
103
+ msg.tool_call_id = m.toolCallId;
104
+ if (m.toolCalls)
105
+ msg.tool_calls = m.toolCalls.map((tc) => ({
106
+ id: tc.id,
107
+ type: "function",
108
+ function: { name: tc.name, arguments: tc.arguments },
109
+ }));
110
+ return msg;
111
+ }),
112
+ };
113
+ if (request.tools?.length) {
114
+ body.tools = request.tools.map((t) => ({
115
+ type: "function",
116
+ function: { name: t.name, description: t.description, parameters: t.parameters },
117
+ }));
118
+ }
119
+ if (request.temperature !== undefined)
120
+ body.temperature = request.temperature;
121
+ if (request.maxTokens !== undefined)
122
+ body.max_tokens = request.maxTokens;
123
+ return body;
124
+ }
125
+ parseChunk(data) {
126
+ const choices = data.choices;
127
+ if (!choices?.length)
128
+ return null;
129
+ const delta = choices[0].delta;
130
+ if (!delta)
131
+ return null;
132
+ const chunk = {};
133
+ if (typeof delta.content === "string")
134
+ chunk.content = delta.content;
135
+ if (choices[0].finish_reason)
136
+ chunk.finishReason = choices[0].finish_reason;
137
+ const toolCalls = delta.tool_calls;
138
+ if (toolCalls) {
139
+ chunk.toolCalls = toolCalls.map((tc) => {
140
+ const fn = tc.function;
141
+ return { id: tc.id, name: fn.name, arguments: fn.arguments };
142
+ });
143
+ }
144
+ if (data.usage) {
145
+ const usage = data.usage;
146
+ chunk.usage = { inputTokens: usage.prompt_tokens ?? 0, outputTokens: usage.completion_tokens ?? 0 };
147
+ }
148
+ return chunk;
149
+ }
150
+ }
151
+ // Anthropic provider (Messages API)
152
+ export class AnthropicProvider extends ModelProvider {
153
+ constructor(profile, logger) {
154
+ super("anthropic", "https://api.anthropic.com/v1", profile, logger);
155
+ }
156
+ async *chat(request) {
157
+ const systemMsg = request.messages.find((m) => m.role === "system");
158
+ const nonSystemMsgs = request.messages.filter((m) => m.role !== "system");
159
+ const body = {
160
+ model: request.model,
161
+ messages: nonSystemMsgs.map((m) => ({ role: m.role, content: m.content })),
162
+ max_tokens: request.maxTokens ?? 4096,
163
+ stream: true,
164
+ };
165
+ if (systemMsg)
166
+ body.system = systemMsg.content;
167
+ if (request.tools?.length) {
168
+ body.tools = request.tools.map((t) => ({
169
+ name: t.name, description: t.description, input_schema: t.parameters,
170
+ }));
171
+ }
172
+ if (request.temperature !== undefined)
173
+ body.temperature = request.temperature;
174
+ const response = await fetch(`${this.baseUrl}/messages`, {
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ "x-api-key": this.activeProfile.apiKey,
179
+ "anthropic-version": "2023-06-01",
180
+ },
181
+ body: JSON.stringify(body),
182
+ });
183
+ if (!response.ok) {
184
+ const errorText = await response.text();
185
+ throw new Error(`Anthropic API error ${response.status}: ${errorText}`);
186
+ }
187
+ const reader = response.body?.getReader();
188
+ if (!reader)
189
+ throw new Error("No response body");
190
+ const decoder = new TextDecoder();
191
+ let buffer = "";
192
+ while (true) {
193
+ const { done, value } = await reader.read();
194
+ if (done)
195
+ break;
196
+ buffer += decoder.decode(value, { stream: true });
197
+ const lines = buffer.split("\n");
198
+ buffer = lines.pop() ?? "";
199
+ for (const line of lines) {
200
+ const trimmed = line.trim();
201
+ if (!trimmed.startsWith("data: "))
202
+ continue;
203
+ try {
204
+ const data = JSON.parse(trimmed.slice(6));
205
+ const chunk = this.parseAnthropicEvent(data);
206
+ if (chunk)
207
+ yield chunk;
208
+ }
209
+ catch {
210
+ // skip
211
+ }
212
+ }
213
+ }
214
+ }
215
+ async listModels() {
216
+ // Anthropic doesn't have a list models endpoint; return known models
217
+ return [
218
+ { id: "claude-opus-4-5", name: "Claude Opus 4.5", contextWindow: 200000 },
219
+ { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", contextWindow: 200000 },
220
+ { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", contextWindow: 200000 },
221
+ ];
222
+ }
223
+ supportsTools() {
224
+ return true;
225
+ }
226
+ supportsStreaming() {
227
+ return true;
228
+ }
229
+ parseAnthropicEvent(data) {
230
+ const type = data.type;
231
+ if (type === "content_block_delta") {
232
+ const delta = data.delta;
233
+ if (delta.type === "text_delta")
234
+ return { content: delta.text };
235
+ if (delta.type === "input_json_delta")
236
+ return null; // handled in tool_use
237
+ }
238
+ if (type === "message_delta") {
239
+ const delta = data.delta;
240
+ const usage = data.usage;
241
+ return {
242
+ finishReason: delta.stop_reason === "tool_use" ? "tool_calls" : "stop",
243
+ usage: usage ? { inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0 } : undefined,
244
+ };
245
+ }
246
+ if (type === "message_start") {
247
+ const message = data.message;
248
+ const usage = message.usage;
249
+ if (usage)
250
+ return { usage: { inputTokens: usage.input_tokens ?? 0, outputTokens: 0 } };
251
+ }
252
+ return null;
253
+ }
254
+ }
255
+ //# sourceMappingURL=model-provider.js.map
@@ -0,0 +1,32 @@
1
+ import type { ChatRequest, ChatChunk, TokenUsage, AuthProfile, Logger } from "./types.js";
2
+ import { ModelProvider } from "./model-provider.js";
3
+ export interface BudgetConfig {
4
+ maxInputTokens?: number;
5
+ maxOutputTokens?: number;
6
+ maxTotalTokens?: number;
7
+ }
8
+ export declare class ModelRouter {
9
+ private providers;
10
+ private aliases;
11
+ private defaultModel;
12
+ private fallbackChain;
13
+ private authProfiles;
14
+ private tokenUsage;
15
+ private budgetLimits;
16
+ private logger;
17
+ constructor(logger: Logger, defaultModel: string);
18
+ registerProvider(provider: ModelProvider): void;
19
+ setAliases(aliases: Record<string, string>): void;
20
+ setFallbackChain(chain: string[]): void;
21
+ setBudget(budget: BudgetConfig): void;
22
+ resolve(modelSpec?: string): string;
23
+ getProvider(modelId: string): ModelProvider | undefined;
24
+ chat(request: ChatRequest, modelSpec?: string): AsyncIterable<ChatChunk>;
25
+ failover(error: Error, currentModel: string): "rotate" | "next" | "compact";
26
+ trackUsage(model: string, tokens: TokenUsage): void;
27
+ getUsage(): Map<string, TokenUsage>;
28
+ checkBudget(): boolean;
29
+ rotateAuthProfile(modelId: string): void;
30
+ setAuthProfiles(providerId: string, profiles: AuthProfile[]): void;
31
+ }
32
+ //# sourceMappingURL=model-router.d.ts.map