@soleri/core 2.0.1 → 2.0.2
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/dist/brain/brain.d.ts +3 -12
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +13 -305
- package/dist/brain/brain.js.map +1 -1
- package/dist/curator/curator.d.ts +28 -0
- package/dist/curator/curator.d.ts.map +1 -0
- package/dist/curator/curator.js +523 -0
- package/dist/curator/curator.js.map +1 -0
- package/dist/curator/types.d.ts +87 -0
- package/dist/curator/types.d.ts.map +1 -0
- package/dist/curator/types.js +3 -0
- package/dist/curator/types.js.map +1 -0
- package/dist/facades/types.d.ts +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts +28 -0
- package/dist/llm/llm-client.d.ts.map +1 -0
- package/dist/llm/llm-client.js +219 -0
- package/dist/llm/llm-client.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +17 -0
- package/dist/runtime/core-ops.d.ts.map +1 -0
- package/dist/runtime/core-ops.js +448 -0
- package/dist/runtime/core-ops.js.map +1 -0
- package/dist/runtime/domain-ops.d.ts +25 -0
- package/dist/runtime/domain-ops.d.ts.map +1 -0
- package/dist/runtime/domain-ops.js +130 -0
- package/dist/runtime/domain-ops.js.map +1 -0
- package/dist/runtime/runtime.d.ts +19 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +62 -0
- package/dist/runtime/runtime.js.map +1 -0
- package/dist/runtime/types.d.ts +39 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/{cognee → runtime}/types.js.map +1 -1
- package/dist/text/similarity.d.ts +8 -0
- package/dist/text/similarity.d.ts.map +1 -0
- package/dist/text/similarity.js +161 -0
- package/dist/text/similarity.js.map +1 -0
- package/package.json +6 -2
- package/src/__tests__/brain.test.ts +27 -265
- package/src/__tests__/core-ops.test.ts +190 -0
- package/src/__tests__/curator.test.ts +479 -0
- package/src/__tests__/domain-ops.test.ts +124 -0
- package/src/__tests__/llm-client.test.ts +69 -0
- package/src/__tests__/runtime.test.ts +93 -0
- package/src/brain/brain.ts +19 -342
- package/src/curator/curator.ts +662 -0
- package/src/curator/types.ts +114 -0
- package/src/index.ts +40 -11
- package/src/llm/llm-client.ts +316 -0
- package/src/runtime/core-ops.ts +472 -0
- package/src/runtime/domain-ops.ts +144 -0
- package/src/runtime/runtime.ts +71 -0
- package/src/runtime/types.ts +37 -0
- package/src/text/similarity.ts +168 -0
- package/dist/cognee/client.d.ts +0 -35
- package/dist/cognee/client.d.ts.map +0 -1
- package/dist/cognee/client.js +0 -291
- package/dist/cognee/client.js.map +0 -1
- package/dist/cognee/types.d.ts +0 -46
- package/dist/cognee/types.d.ts.map +0 -1
- package/dist/cognee/types.js +0 -3
- package/src/__tests__/cognee-client.test.ts +0 -524
- package/src/cognee/client.ts +0 -352
- package/src/cognee/types.ts +0 -62
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// ─── Curator Types ──────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type EntryStatus = 'active' | 'stale' | 'archived';
|
|
4
|
+
export type EntrySource = 'manual' | 'capture' | 'seed' | 'unknown';
|
|
5
|
+
|
|
6
|
+
// ─── Tag Normalization ──────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface TagNormalizationResult {
|
|
9
|
+
original: string;
|
|
10
|
+
normalized: string;
|
|
11
|
+
wasAliased: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CanonicalTag {
|
|
15
|
+
tag: string;
|
|
16
|
+
description: string | null;
|
|
17
|
+
usageCount: number;
|
|
18
|
+
aliasCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Duplicate Detection ────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface DuplicateCandidate {
|
|
24
|
+
entryId: string;
|
|
25
|
+
title: string;
|
|
26
|
+
similarity: number;
|
|
27
|
+
suggestMerge: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DuplicateDetectionResult {
|
|
31
|
+
entryId: string;
|
|
32
|
+
matches: DuplicateCandidate[];
|
|
33
|
+
scannedCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Contradictions ─────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export type ContradictionStatus = 'open' | 'resolved' | 'dismissed';
|
|
39
|
+
|
|
40
|
+
export interface Contradiction {
|
|
41
|
+
id: number;
|
|
42
|
+
patternId: string;
|
|
43
|
+
antipatternId: string;
|
|
44
|
+
similarity: number;
|
|
45
|
+
status: ContradictionStatus;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
resolvedAt: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Grooming ───────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export interface GroomResult {
|
|
53
|
+
entryId: string;
|
|
54
|
+
tagsNormalized: TagNormalizationResult[];
|
|
55
|
+
stale: boolean;
|
|
56
|
+
lastGroomedAt: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GroomAllResult {
|
|
60
|
+
totalEntries: number;
|
|
61
|
+
groomedCount: number;
|
|
62
|
+
tagsNormalized: number;
|
|
63
|
+
staleCount: number;
|
|
64
|
+
durationMs: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Consolidation ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface ConsolidationOptions {
|
|
70
|
+
dryRun?: boolean;
|
|
71
|
+
staleDaysThreshold?: number;
|
|
72
|
+
duplicateThreshold?: number;
|
|
73
|
+
contradictionThreshold?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ConsolidationResult {
|
|
77
|
+
dryRun: boolean;
|
|
78
|
+
duplicates: DuplicateDetectionResult[];
|
|
79
|
+
staleEntries: string[];
|
|
80
|
+
contradictions: Contradiction[];
|
|
81
|
+
mutations: number;
|
|
82
|
+
durationMs: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Changelog & Health ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export interface ChangelogEntry {
|
|
88
|
+
id: number;
|
|
89
|
+
action: string;
|
|
90
|
+
entryId: string;
|
|
91
|
+
beforeValue: string | null;
|
|
92
|
+
afterValue: string | null;
|
|
93
|
+
reason: string;
|
|
94
|
+
createdAt: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface HealthMetrics {
|
|
98
|
+
coverage: number;
|
|
99
|
+
freshness: number;
|
|
100
|
+
quality: number;
|
|
101
|
+
tagHealth: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface HealthAuditResult {
|
|
105
|
+
score: number;
|
|
106
|
+
metrics: HealthMetrics;
|
|
107
|
+
recommendations: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CuratorStatus {
|
|
111
|
+
initialized: boolean;
|
|
112
|
+
tables: Record<string, number>;
|
|
113
|
+
lastGroomedAt: number | null;
|
|
114
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,37 @@ export { loadIntelligenceData } from './intelligence/loader.js';
|
|
|
6
6
|
export { Vault } from './vault/vault.js';
|
|
7
7
|
export type { SearchResult, VaultStats, ProjectInfo, Memory, MemoryStats } from './vault/vault.js';
|
|
8
8
|
|
|
9
|
+
// ─── Text Utilities ─────────────────────────────────────────────────
|
|
10
|
+
export {
|
|
11
|
+
tokenize,
|
|
12
|
+
calculateTf,
|
|
13
|
+
calculateTfIdf,
|
|
14
|
+
cosineSimilarity,
|
|
15
|
+
jaccardSimilarity,
|
|
16
|
+
} from './text/similarity.js';
|
|
17
|
+
export type { SparseVector } from './text/similarity.js';
|
|
18
|
+
|
|
19
|
+
// ─── Curator ────────────────────────────────────────────────────────
|
|
20
|
+
export { Curator } from './curator/curator.js';
|
|
21
|
+
export type {
|
|
22
|
+
EntryStatus,
|
|
23
|
+
EntrySource,
|
|
24
|
+
TagNormalizationResult,
|
|
25
|
+
CanonicalTag,
|
|
26
|
+
DuplicateCandidate,
|
|
27
|
+
DuplicateDetectionResult,
|
|
28
|
+
Contradiction,
|
|
29
|
+
ContradictionStatus,
|
|
30
|
+
GroomResult,
|
|
31
|
+
GroomAllResult,
|
|
32
|
+
ConsolidationOptions,
|
|
33
|
+
ConsolidationResult,
|
|
34
|
+
ChangelogEntry,
|
|
35
|
+
HealthMetrics,
|
|
36
|
+
HealthAuditResult,
|
|
37
|
+
CuratorStatus,
|
|
38
|
+
} from './curator/types.js';
|
|
39
|
+
|
|
9
40
|
// ─── Brain ───────────────────────────────────────────────────────────
|
|
10
41
|
export { Brain } from './brain/brain.js';
|
|
11
42
|
export type {
|
|
@@ -18,17 +49,6 @@ export type {
|
|
|
18
49
|
QueryContext,
|
|
19
50
|
} from './brain/brain.js';
|
|
20
51
|
|
|
21
|
-
// ─── Cognee ─────────────────────────────────────────────────────────
|
|
22
|
-
export { CogneeClient } from './cognee/client.js';
|
|
23
|
-
export type {
|
|
24
|
-
CogneeConfig,
|
|
25
|
-
CogneeSearchResult,
|
|
26
|
-
CogneeSearchType,
|
|
27
|
-
CogneeStatus,
|
|
28
|
-
CogneeAddResult,
|
|
29
|
-
CogneeCognifyResult,
|
|
30
|
-
} from './cognee/types.js';
|
|
31
|
-
|
|
32
52
|
// ─── Planning ────────────────────────────────────────────────────────
|
|
33
53
|
export { Planner } from './planning/planner.js';
|
|
34
54
|
export type { PlanStatus, TaskStatus, PlanTask, Plan, PlanStore } from './planning/planner.js';
|
|
@@ -73,3 +93,12 @@ export type {
|
|
|
73
93
|
FacadeResponse,
|
|
74
94
|
FacadeInput,
|
|
75
95
|
} from './facades/types.js';
|
|
96
|
+
|
|
97
|
+
// ─── LLM Client ─────────────────────────────────────────────────────
|
|
98
|
+
export { LLMClient } from './llm/llm-client.js';
|
|
99
|
+
|
|
100
|
+
// ─── Runtime Factory ────────────────────────────────────────────────
|
|
101
|
+
export { createAgentRuntime } from './runtime/runtime.js';
|
|
102
|
+
export { createCoreOps } from './runtime/core-ops.js';
|
|
103
|
+
export { createDomainFacade, createDomainFacades } from './runtime/domain-ops.js';
|
|
104
|
+
export type { AgentRuntimeConfig, AgentRuntime } from './runtime/types.js';
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Client — Unified OpenAI/Anthropic caller with key pool rotation,
|
|
3
|
+
* circuit breaker, retry, and model routing.
|
|
4
|
+
*
|
|
5
|
+
* Anthropic SDK is loaded via dynamic import on first use, keeping it
|
|
6
|
+
* an optional peer dependency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { LLMError } from './types.js';
|
|
13
|
+
import { CircuitBreaker, retry, parseRateLimitHeaders } from './utils.js';
|
|
14
|
+
import type {
|
|
15
|
+
LLMCallOptions,
|
|
16
|
+
LLMCallResult,
|
|
17
|
+
RouteEntry,
|
|
18
|
+
RoutingConfig,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
import type { KeyPool } from './key-pool.js';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// CONSTANTS
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// MODEL ROUTER
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
function loadRoutingConfig(agentId: string): RoutingConfig {
|
|
33
|
+
const defaultConfig: RoutingConfig = {
|
|
34
|
+
routes: [],
|
|
35
|
+
defaultOpenAIModel: 'gpt-4o-mini',
|
|
36
|
+
defaultAnthropicModel: 'claude-sonnet-4-20250514',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const configPath = path.join(homedir(), `.${agentId}`, 'model-routing.json');
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(configPath)) {
|
|
43
|
+
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Partial<RoutingConfig>;
|
|
44
|
+
if (data.routes && Array.isArray(data.routes)) {
|
|
45
|
+
defaultConfig.routes = data.routes;
|
|
46
|
+
}
|
|
47
|
+
if (data.defaultOpenAIModel) {
|
|
48
|
+
defaultConfig.defaultOpenAIModel = data.defaultOpenAIModel;
|
|
49
|
+
}
|
|
50
|
+
if (data.defaultAnthropicModel) {
|
|
51
|
+
defaultConfig.defaultAnthropicModel = data.defaultAnthropicModel;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Config not available — use defaults
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return defaultConfig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function inferProvider(model: string): 'openai' | 'anthropic' {
|
|
62
|
+
if (model.startsWith('claude-') || model.startsWith('anthropic/')) {
|
|
63
|
+
return 'anthropic';
|
|
64
|
+
}
|
|
65
|
+
return 'openai';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class ModelRouter {
|
|
69
|
+
private config: RoutingConfig;
|
|
70
|
+
|
|
71
|
+
constructor(config?: RoutingConfig) {
|
|
72
|
+
this.config = config ?? { routes: [], defaultOpenAIModel: 'gpt-4o-mini', defaultAnthropicModel: 'claude-sonnet-4-20250514' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
resolve(
|
|
76
|
+
caller: string,
|
|
77
|
+
task?: string,
|
|
78
|
+
originalModel?: string,
|
|
79
|
+
): { model: string; provider: 'openai' | 'anthropic' } {
|
|
80
|
+
if (task) {
|
|
81
|
+
const exactMatch = this.config.routes.find(
|
|
82
|
+
(r) => r.caller === caller && r.task === task,
|
|
83
|
+
);
|
|
84
|
+
if (exactMatch) {
|
|
85
|
+
return { model: exactMatch.model, provider: exactMatch.provider };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const callerMatch = this.config.routes.find(
|
|
90
|
+
(r) => r.caller === caller && !r.task,
|
|
91
|
+
);
|
|
92
|
+
if (callerMatch) {
|
|
93
|
+
return { model: callerMatch.model, provider: callerMatch.provider };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (originalModel) {
|
|
97
|
+
const provider = inferProvider(originalModel);
|
|
98
|
+
return { model: originalModel, provider };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { model: this.config.defaultOpenAIModel, provider: 'openai' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getRoutes(): RouteEntry[] {
|
|
105
|
+
return [...this.config.routes];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// LLM CLIENT
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
// Anthropic SDK type — we only need the messages.create method shape
|
|
114
|
+
interface AnthropicClient {
|
|
115
|
+
messages: {
|
|
116
|
+
create(
|
|
117
|
+
params: {
|
|
118
|
+
model: string;
|
|
119
|
+
max_tokens: number;
|
|
120
|
+
system: string;
|
|
121
|
+
messages: Array<{ role: string; content: string }>;
|
|
122
|
+
},
|
|
123
|
+
options?: { timeout: number },
|
|
124
|
+
): Promise<{
|
|
125
|
+
content: Array<{ type: string; text?: string }>;
|
|
126
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
127
|
+
}>;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class LLMClient {
|
|
132
|
+
private openaiKeyPool: KeyPool;
|
|
133
|
+
private anthropicKeyPool: KeyPool;
|
|
134
|
+
private anthropicClient: AnthropicClient | null = null;
|
|
135
|
+
private anthropicBreaker: CircuitBreaker;
|
|
136
|
+
private anthropicKeyFingerprint: string = '';
|
|
137
|
+
private router: ModelRouter;
|
|
138
|
+
|
|
139
|
+
constructor(openaiKeyPool: KeyPool, anthropicKeyPool: KeyPool, agentId?: string) {
|
|
140
|
+
this.openaiKeyPool = openaiKeyPool;
|
|
141
|
+
this.anthropicKeyPool = anthropicKeyPool;
|
|
142
|
+
this.anthropicBreaker = new CircuitBreaker({
|
|
143
|
+
name: 'llm-anthropic',
|
|
144
|
+
failureThreshold: 5,
|
|
145
|
+
resetTimeoutMs: 60_000,
|
|
146
|
+
});
|
|
147
|
+
this.router = new ModelRouter(agentId ? loadRoutingConfig(agentId) : undefined);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async complete(options: LLMCallOptions): Promise<LLMCallResult> {
|
|
151
|
+
const routed = this.router.resolve(
|
|
152
|
+
options.caller,
|
|
153
|
+
options.task,
|
|
154
|
+
options.model,
|
|
155
|
+
);
|
|
156
|
+
const resolvedOptions = { ...options, model: routed.model, provider: routed.provider };
|
|
157
|
+
|
|
158
|
+
return resolvedOptions.provider === 'anthropic'
|
|
159
|
+
? this.callAnthropic(resolvedOptions)
|
|
160
|
+
: this.callOpenAI(resolvedOptions);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
isAvailable(): { openai: boolean; anthropic: boolean } {
|
|
164
|
+
return {
|
|
165
|
+
openai: this.openaiKeyPool.hasKeys,
|
|
166
|
+
anthropic: this.anthropicKeyPool.hasKeys,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getRoutes(): RouteEntry[] {
|
|
171
|
+
return this.router.getRoutes();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
// OPENAI
|
|
176
|
+
// ===========================================================================
|
|
177
|
+
|
|
178
|
+
private async callOpenAI(options: LLMCallOptions): Promise<LLMCallResult> {
|
|
179
|
+
const keyPool = this.openaiKeyPool.hasKeys ? this.openaiKeyPool : null;
|
|
180
|
+
|
|
181
|
+
if (!keyPool) {
|
|
182
|
+
throw new LLMError('OpenAI API key not configured', { retryable: false });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const start = Date.now();
|
|
186
|
+
|
|
187
|
+
const doRequest = async (): Promise<LLMCallResult> => {
|
|
188
|
+
const apiKey = keyPool.getActiveKey().expose();
|
|
189
|
+
const keyIndex = keyPool.activeKeyIndex;
|
|
190
|
+
|
|
191
|
+
const response = await fetch(OPENAI_API_URL, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
Authorization: `Bearer ${apiKey}`,
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
model: options.model,
|
|
199
|
+
messages: [
|
|
200
|
+
{ role: 'system', content: options.systemPrompt },
|
|
201
|
+
{ role: 'user', content: options.userPrompt },
|
|
202
|
+
],
|
|
203
|
+
temperature: options.temperature ?? 0.3,
|
|
204
|
+
max_completion_tokens: options.maxTokens ?? 500,
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (response.headers) {
|
|
209
|
+
const rateLimits = parseRateLimitHeaders(response.headers);
|
|
210
|
+
if (rateLimits.remaining !== null) {
|
|
211
|
+
keyPool.updateQuota(keyIndex, rateLimits.remaining);
|
|
212
|
+
keyPool.rotatePreemptive();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
if (response.status === 429 && keyPool.poolSize > 1) {
|
|
218
|
+
keyPool.rotateOnError();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const errorBody = await response.text();
|
|
222
|
+
throw new LLMError(
|
|
223
|
+
`OpenAI API error: ${response.status} - ${errorBody}`,
|
|
224
|
+
{ retryable: response.status === 429 || response.status >= 500, statusCode: response.status },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const data = (await response.json()) as {
|
|
229
|
+
choices: Array<{ message: { content: string } }>;
|
|
230
|
+
usage?: { prompt_tokens?: number; completion_tokens?: number };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
text: data.choices[0]?.message?.content || '',
|
|
235
|
+
model: options.model,
|
|
236
|
+
provider: 'openai' as const,
|
|
237
|
+
inputTokens: data.usage?.prompt_tokens,
|
|
238
|
+
outputTokens: data.usage?.completion_tokens,
|
|
239
|
+
durationMs: Date.now() - start,
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return retry(doRequest, { maxAttempts: 3 });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ===========================================================================
|
|
247
|
+
// ANTHROPIC
|
|
248
|
+
// ===========================================================================
|
|
249
|
+
|
|
250
|
+
private async callAnthropic(options: LLMCallOptions): Promise<LLMCallResult> {
|
|
251
|
+
const client = await this.getAnthropicClient();
|
|
252
|
+
if (!client) {
|
|
253
|
+
throw new LLMError('Anthropic API key not configured', { retryable: false });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const start = Date.now();
|
|
257
|
+
|
|
258
|
+
return this.anthropicBreaker.call(() =>
|
|
259
|
+
retry(
|
|
260
|
+
async () => {
|
|
261
|
+
const response = await client.messages.create(
|
|
262
|
+
{
|
|
263
|
+
model: options.model,
|
|
264
|
+
max_tokens: options.maxTokens ?? 1024,
|
|
265
|
+
system: options.systemPrompt,
|
|
266
|
+
messages: [{ role: 'user', content: options.userPrompt }],
|
|
267
|
+
},
|
|
268
|
+
{ timeout: 60_000 },
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const text = response.content
|
|
272
|
+
.filter(
|
|
273
|
+
(block): block is { type: 'text'; text: string } => block.type === 'text' && typeof block.text === 'string',
|
|
274
|
+
)
|
|
275
|
+
.map((block) => block.text)
|
|
276
|
+
.join('\n');
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
text,
|
|
280
|
+
model: options.model,
|
|
281
|
+
provider: 'anthropic' as const,
|
|
282
|
+
inputTokens: response.usage?.input_tokens,
|
|
283
|
+
outputTokens: response.usage?.output_tokens,
|
|
284
|
+
durationMs: Date.now() - start,
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
{ maxAttempts: 2 },
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async getAnthropicClient(): Promise<AnthropicClient | null> {
|
|
293
|
+
if (!this.anthropicKeyPool.hasKeys) return null;
|
|
294
|
+
|
|
295
|
+
const currentKey = this.anthropicKeyPool.getActiveKey().expose();
|
|
296
|
+
const currentFingerprint = currentKey.slice(-8);
|
|
297
|
+
|
|
298
|
+
if (currentFingerprint !== this.anthropicKeyFingerprint) {
|
|
299
|
+
this.anthropicClient = null;
|
|
300
|
+
this.anthropicKeyFingerprint = currentFingerprint;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this.anthropicClient) return this.anthropicClient;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Dynamic import — @anthropic-ai/sdk is an optional peer dep.
|
|
307
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
308
|
+
const mod = await (Function('return import("@anthropic-ai/sdk")')() as Promise<{ default: new (opts: { apiKey: string }) => AnthropicClient }>);
|
|
309
|
+
this.anthropicClient = new mod.default({ apiKey: currentKey });
|
|
310
|
+
return this.anthropicClient;
|
|
311
|
+
} catch {
|
|
312
|
+
// SDK not installed — Anthropic provider unavailable
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|