adonis-ai-kit 0.0.0-stage → 1.2.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 +74 -0
- package/LICENSE +21 -0
- package/README.md +397 -2
- package/dist/commands/ai_test.d.ts +12 -0
- package/dist/commands/ai_test.d.ts.map +1 -0
- package/dist/commands/ai_test.js +98 -0
- package/dist/commands/ai_test.js.map +1 -0
- package/dist/commands/main.d.ts +14 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +31 -0
- package/dist/commands/main.js.map +1 -0
- package/dist/commands/make_agent.d.ts +13 -0
- package/dist/commands/make_agent.d.ts.map +1 -0
- package/dist/commands/make_agent.js +116 -0
- package/dist/commands/make_agent.js.map +1 -0
- package/dist/providers/ai_provider.d.ts +23 -0
- package/dist/providers/ai_provider.d.ts.map +1 -0
- package/dist/providers/ai_provider.js +34 -0
- package/dist/providers/ai_provider.js.map +1 -0
- package/dist/services/main.d.ts +4 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +7 -0
- package/dist/services/main.js.map +1 -0
- package/dist/src/acp/acp_agents.d.ts +89 -0
- package/dist/src/acp/acp_agents.d.ts.map +1 -0
- package/dist/src/acp/acp_agents.js +367 -0
- package/dist/src/acp/acp_agents.js.map +1 -0
- package/dist/src/acp/index.d.ts +4 -0
- package/dist/src/acp/index.d.ts.map +1 -0
- package/dist/src/acp/index.js +4 -0
- package/dist/src/acp/index.js.map +1 -0
- package/dist/src/acp/provider.d.ts +74 -0
- package/dist/src/acp/provider.d.ts.map +1 -0
- package/dist/src/acp/provider.js +276 -0
- package/dist/src/acp/provider.js.map +1 -0
- package/dist/src/acp/types.d.ts +128 -0
- package/dist/src/acp/types.d.ts.map +1 -0
- package/dist/src/acp/types.js +2 -0
- package/dist/src/acp/types.js.map +1 -0
- package/dist/src/agents/agent_runner.d.ts +60 -0
- package/dist/src/agents/agent_runner.d.ts.map +1 -0
- package/dist/src/agents/agent_runner.js +134 -0
- package/dist/src/agents/agent_runner.js.map +1 -0
- package/dist/src/agents/index.d.ts +4 -0
- package/dist/src/agents/index.d.ts.map +1 -0
- package/dist/src/agents/index.js +4 -0
- package/dist/src/agents/index.js.map +1 -0
- package/dist/src/agents/loader.d.ts +56 -0
- package/dist/src/agents/loader.d.ts.map +1 -0
- package/dist/src/agents/loader.js +184 -0
- package/dist/src/agents/loader.js.map +1 -0
- package/dist/src/agents/manifest.d.ts +61 -0
- package/dist/src/agents/manifest.d.ts.map +1 -0
- package/dist/src/agents/manifest.js +35 -0
- package/dist/src/agents/manifest.js.map +1 -0
- package/dist/src/ai_manager.d.ts +140 -0
- package/dist/src/ai_manager.d.ts.map +1 -0
- package/dist/src/ai_manager.js +814 -0
- package/dist/src/ai_manager.js.map +1 -0
- package/dist/src/budget/budget_allocator.d.ts +37 -0
- package/dist/src/budget/budget_allocator.d.ts.map +1 -0
- package/dist/src/budget/budget_allocator.js +98 -0
- package/dist/src/budget/budget_allocator.js.map +1 -0
- package/dist/src/budget/budget_enforcer.d.ts +70 -0
- package/dist/src/budget/budget_enforcer.d.ts.map +1 -0
- package/dist/src/budget/budget_enforcer.js +195 -0
- package/dist/src/budget/budget_enforcer.js.map +1 -0
- package/dist/src/budget/cost_tracker.d.ts +71 -0
- package/dist/src/budget/cost_tracker.d.ts.map +1 -0
- package/dist/src/budget/cost_tracker.js +199 -0
- package/dist/src/budget/cost_tracker.js.map +1 -0
- package/dist/src/budget/index.d.ts +6 -0
- package/dist/src/budget/index.d.ts.map +1 -0
- package/dist/src/budget/index.js +6 -0
- package/dist/src/budget/index.js.map +1 -0
- package/dist/src/budget/pricing.d.ts +35 -0
- package/dist/src/budget/pricing.d.ts.map +1 -0
- package/dist/src/budget/pricing.js +129 -0
- package/dist/src/budget/pricing.js.map +1 -0
- package/dist/src/budget/types.d.ts +86 -0
- package/dist/src/budget/types.d.ts.map +1 -0
- package/dist/src/budget/types.js +2 -0
- package/dist/src/budget/types.js.map +1 -0
- package/dist/src/catalog/index.d.ts +3 -0
- package/dist/src/catalog/index.d.ts.map +1 -0
- package/dist/src/catalog/index.js +3 -0
- package/dist/src/catalog/index.js.map +1 -0
- package/dist/src/catalog/models_dev.d.ts +87 -0
- package/dist/src/catalog/models_dev.d.ts.map +1 -0
- package/dist/src/catalog/models_dev.js +38 -0
- package/dist/src/catalog/models_dev.js.map +1 -0
- package/dist/src/catalog/source.d.ts +67 -0
- package/dist/src/catalog/source.d.ts.map +1 -0
- package/dist/src/catalog/source.js +678 -0
- package/dist/src/catalog/source.js.map +1 -0
- package/dist/src/circuit_breaker.d.ts +61 -0
- package/dist/src/circuit_breaker.d.ts.map +1 -0
- package/dist/src/circuit_breaker.js +217 -0
- package/dist/src/circuit_breaker.js.map +1 -0
- package/dist/src/define_config.d.ts +11 -0
- package/dist/src/define_config.d.ts.map +1 -0
- package/dist/src/define_config.js +48 -0
- package/dist/src/define_config.js.map +1 -0
- package/dist/src/error_classifier.d.ts +50 -0
- package/dist/src/error_classifier.d.ts.map +1 -0
- package/dist/src/error_classifier.js +206 -0
- package/dist/src/error_classifier.js.map +1 -0
- package/dist/src/errors.d.ts +52 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +70 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/guard/confirmation.d.ts +67 -0
- package/dist/src/guard/confirmation.d.ts.map +1 -0
- package/dist/src/guard/confirmation.js +180 -0
- package/dist/src/guard/confirmation.js.map +1 -0
- package/dist/src/guard/errors.d.ts +10 -0
- package/dist/src/guard/errors.d.ts.map +1 -0
- package/dist/src/guard/errors.js +14 -0
- package/dist/src/guard/errors.js.map +1 -0
- package/dist/src/guard/guard.d.ts +47 -0
- package/dist/src/guard/guard.d.ts.map +1 -0
- package/dist/src/guard/guard.js +133 -0
- package/dist/src/guard/guard.js.map +1 -0
- package/dist/src/guard/index.d.ts +7 -0
- package/dist/src/guard/index.d.ts.map +1 -0
- package/dist/src/guard/index.js +7 -0
- package/dist/src/guard/index.js.map +1 -0
- package/dist/src/guard/registry.d.ts +72 -0
- package/dist/src/guard/registry.d.ts.map +1 -0
- package/dist/src/guard/registry.js +192 -0
- package/dist/src/guard/registry.js.map +1 -0
- package/dist/src/guard/schema_registry.d.ts +37 -0
- package/dist/src/guard/schema_registry.d.ts.map +1 -0
- package/dist/src/guard/schema_registry.js +57 -0
- package/dist/src/guard/schema_registry.js.map +1 -0
- package/dist/src/guard/types.d.ts +63 -0
- package/dist/src/guard/types.d.ts.map +1 -0
- package/dist/src/guard/types.js +2 -0
- package/dist/src/guard/types.js.map +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +14 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +7 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +2 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/models/catalog_resolver.d.ts +24 -0
- package/dist/src/models/catalog_resolver.d.ts.map +1 -0
- package/dist/src/models/catalog_resolver.js +66 -0
- package/dist/src/models/catalog_resolver.js.map +1 -0
- package/dist/src/models/index.d.ts +14 -0
- package/dist/src/models/index.d.ts.map +1 -0
- package/dist/src/models/index.js +8 -0
- package/dist/src/models/index.js.map +1 -0
- package/dist/src/models/keys.d.ts +9 -0
- package/dist/src/models/keys.d.ts.map +1 -0
- package/dist/src/models/keys.js +12 -0
- package/dist/src/models/keys.js.map +1 -0
- package/dist/src/models/language_model_factory.d.ts +44 -0
- package/dist/src/models/language_model_factory.d.ts.map +1 -0
- package/dist/src/models/language_model_factory.js +193 -0
- package/dist/src/models/language_model_factory.js.map +1 -0
- package/dist/src/models/model_chain.d.ts +58 -0
- package/dist/src/models/model_chain.d.ts.map +1 -0
- package/dist/src/models/model_chain.js +144 -0
- package/dist/src/models/model_chain.js.map +1 -0
- package/dist/src/models/model_ref.d.ts +12 -0
- package/dist/src/models/model_ref.d.ts.map +1 -0
- package/dist/src/models/model_ref.js +33 -0
- package/dist/src/models/model_ref.js.map +1 -0
- package/dist/src/models/protocols.d.ts +6 -0
- package/dist/src/models/protocols.d.ts.map +1 -0
- package/dist/src/models/protocols.js +33 -0
- package/dist/src/models/protocols.js.map +1 -0
- package/dist/src/models/provider_factories.d.ts +19 -0
- package/dist/src/models/provider_factories.d.ts.map +1 -0
- package/dist/src/models/provider_factories.js +70 -0
- package/dist/src/models/provider_factories.js.map +1 -0
- package/dist/src/models/types.d.ts +17 -0
- package/dist/src/models/types.d.ts.map +1 -0
- package/dist/src/models/types.js +2 -0
- package/dist/src/models/types.js.map +1 -0
- package/dist/src/response_macro.d.ts +16 -0
- package/dist/src/response_macro.d.ts.map +1 -0
- package/dist/src/response_macro.js +151 -0
- package/dist/src/response_macro.js.map +1 -0
- package/dist/src/types.d.ts +340 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/usage.d.ts +13 -0
- package/dist/src/usage.d.ts.map +1 -0
- package/dist/src/usage.js +61 -0
- package/dist/src/usage.js.map +1 -0
- package/package.json +208 -4
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { generateText, streamText, stepCountIs, Output } from 'ai';
|
|
10
|
+
import { CostTracker } from './budget/cost_tracker.js';
|
|
11
|
+
import { ConfirmationService } from './guard/confirmation.js';
|
|
12
|
+
import { ToolRegistry } from './guard/registry.js';
|
|
13
|
+
import { CircuitBreaker } from './circuit_breaker.js';
|
|
14
|
+
import { AiException, CascadingFallbackError, ProviderConfigError, } from './errors.js';
|
|
15
|
+
import { classifyError } from './error_classifier.js';
|
|
16
|
+
import { EMPTY_USAGE, normalizeUsage, sumUsage } from './usage.js';
|
|
17
|
+
import { ModelsDevSource, resolveCatalogProviderId } from './catalog/source.js';
|
|
18
|
+
import { AgentLoader } from './agents/loader.js';
|
|
19
|
+
import { AgentRunner } from './agents/agent_runner.js';
|
|
20
|
+
import { LanguageModelFactory, buildChainCandidates, runModelChain, } from './models/index.js';
|
|
21
|
+
/** `ResolvedModel` entries keep their order; strings become `ref` candidates. Same dedupe rules as `buildChainCandidates`. */
|
|
22
|
+
function toChainCandidates(models) {
|
|
23
|
+
if (models.every((m) => typeof m !== 'string')) {
|
|
24
|
+
return buildChainCandidates({ chain: models });
|
|
25
|
+
}
|
|
26
|
+
const candidates = [];
|
|
27
|
+
const seenRefs = new Set();
|
|
28
|
+
const seenEntries = new Set();
|
|
29
|
+
for (const entry of models) {
|
|
30
|
+
if (typeof entry === 'string') {
|
|
31
|
+
if (!entry || seenRefs.has(entry))
|
|
32
|
+
continue;
|
|
33
|
+
seenRefs.add(entry);
|
|
34
|
+
candidates.push({ kind: 'ref', ref: entry, key: entry });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const [candidate] = buildChainCandidates({ chain: [entry] });
|
|
38
|
+
const entryId = `${candidate.key}|${entry.providerID}/${entry.modelID}`;
|
|
39
|
+
if (seenEntries.has(entryId))
|
|
40
|
+
continue;
|
|
41
|
+
seenEntries.add(entryId);
|
|
42
|
+
candidates.push(candidate);
|
|
43
|
+
}
|
|
44
|
+
return candidates;
|
|
45
|
+
}
|
|
46
|
+
const PRICING_WAIT_MS = 2_000;
|
|
47
|
+
function sleep(ms) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const timer = setTimeout(resolve, ms);
|
|
50
|
+
timer.unref?.();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Central AI Manager for AdonisJS 7 (analog of Laravel AI / Prism).
|
|
55
|
+
* Orchestrates multi-provider drivers, models.dev catalog discovery, ACP agents,
|
|
56
|
+
* declarative skills & agent runners, cascading failover, circuit breaker resilience,
|
|
57
|
+
* token budget accounting, and guarded tool execution.
|
|
58
|
+
*/
|
|
59
|
+
export class AiManager {
|
|
60
|
+
config;
|
|
61
|
+
logger;
|
|
62
|
+
circuitBreaker;
|
|
63
|
+
costTracker;
|
|
64
|
+
toolRegistry;
|
|
65
|
+
confirmation;
|
|
66
|
+
catalog;
|
|
67
|
+
agentLoader;
|
|
68
|
+
drivers = new Map();
|
|
69
|
+
pricingWarmups = new Map();
|
|
70
|
+
lazyLanguageModels;
|
|
71
|
+
constructor(config, logger, costTracker, toolRegistry, catalog, agentLoader, confirmation) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
this.logger = logger;
|
|
74
|
+
this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
|
|
75
|
+
this.costTracker =
|
|
76
|
+
costTracker ??
|
|
77
|
+
new CostTracker({
|
|
78
|
+
pricing: config.budget?.pricing,
|
|
79
|
+
logger: this.logger,
|
|
80
|
+
});
|
|
81
|
+
this.toolRegistry = toolRegistry ?? new ToolRegistry({ logger: this.logger });
|
|
82
|
+
this.confirmation = confirmation ?? new ConfirmationService({ logger: this.logger });
|
|
83
|
+
this.catalog =
|
|
84
|
+
catalog ??
|
|
85
|
+
new ModelsDevSource({
|
|
86
|
+
logger: this.logger,
|
|
87
|
+
cachePath: config.catalog?.cachePath,
|
|
88
|
+
overridePath: config.catalog?.overridePath,
|
|
89
|
+
ttlMs: config.catalog?.ttlMs,
|
|
90
|
+
systemProviders: config.catalog?.systemProviders,
|
|
91
|
+
});
|
|
92
|
+
this.agentLoader =
|
|
93
|
+
agentLoader ??
|
|
94
|
+
new AgentLoader({
|
|
95
|
+
configsDir: config.agentConfigsDir,
|
|
96
|
+
skillsDir: config.agentSkillsDir,
|
|
97
|
+
logger: this.logger,
|
|
98
|
+
});
|
|
99
|
+
// Setup dynamic pricing loader from models.dev catalog
|
|
100
|
+
if (this.config.catalog?.autoPricing !== false) {
|
|
101
|
+
this.costTracker.registerPricingLoader(async () => {
|
|
102
|
+
const models = await this.catalog.listModels();
|
|
103
|
+
const pricingMap = {};
|
|
104
|
+
for (const m of models) {
|
|
105
|
+
if (m.pricing) {
|
|
106
|
+
pricingMap[m.id] = m.pricing;
|
|
107
|
+
pricingMap[`${m.providerId}/${m.id}`] = m.pricing;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return pricingMap;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
getConfig() {
|
|
115
|
+
return this.config;
|
|
116
|
+
}
|
|
117
|
+
getCircuitBreaker() {
|
|
118
|
+
return this.circuitBreaker;
|
|
119
|
+
}
|
|
120
|
+
getCostTracker() {
|
|
121
|
+
return this.costTracker;
|
|
122
|
+
}
|
|
123
|
+
getToolRegistry() {
|
|
124
|
+
return this.toolRegistry;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Service that answers `ask` tool permissions and ACP agent permission requests.
|
|
128
|
+
* Subscribe with `onActionCreated()` and call `approve()` / `reject()` from your UI.
|
|
129
|
+
*/
|
|
130
|
+
getConfirmationService() {
|
|
131
|
+
return this.confirmation;
|
|
132
|
+
}
|
|
133
|
+
getCatalog() {
|
|
134
|
+
return this.catalog;
|
|
135
|
+
}
|
|
136
|
+
getAgentLoader() {
|
|
137
|
+
return this.agentLoader;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Instantiates an executable AgentRunner by loading its markdown manifest and skills.
|
|
141
|
+
*/
|
|
142
|
+
async agent(name, options) {
|
|
143
|
+
const promptConfig = await this.agentLoader.loadAgentPrompt(name);
|
|
144
|
+
return new AgentRunner(this, promptConfig, options);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Loads all agent prompt configurations from the configured directory.
|
|
148
|
+
*/
|
|
149
|
+
async loadAgents(configsDir, skillsDir) {
|
|
150
|
+
return this.agentLoader.loadAllAgentPrompts(configsDir, skillsDir);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Dynamically resolves a model reference string (e.g. 'openai/gpt-4o', 'claude-3-5-sonnet')
|
|
154
|
+
* against the models.dev catalog.
|
|
155
|
+
*/
|
|
156
|
+
async resolveModel(modelRef, defaultProvider) {
|
|
157
|
+
return this.catalog.resolveModel(modelRef, defaultProvider ?? this.config.default);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Returns a specific driver by name, or the default driver.
|
|
161
|
+
* Concurrent callers share one driver instance.
|
|
162
|
+
*/
|
|
163
|
+
async use(name) {
|
|
164
|
+
const providerName = name ?? this.config.default;
|
|
165
|
+
const existing = this.drivers.get(providerName);
|
|
166
|
+
if (existing) {
|
|
167
|
+
return existing;
|
|
168
|
+
}
|
|
169
|
+
const providerConfig = this.config.providers[providerName];
|
|
170
|
+
if (!providerConfig) {
|
|
171
|
+
throw new ProviderConfigError(providerName, `Provider "${providerName}" is not registered in config.`);
|
|
172
|
+
}
|
|
173
|
+
const pending = this.createDriver(providerName, providerConfig);
|
|
174
|
+
this.drivers.set(providerName, pending);
|
|
175
|
+
pending.catch(() => {
|
|
176
|
+
if (this.drivers.get(providerName) === pending)
|
|
177
|
+
this.drivers.delete(providerName);
|
|
178
|
+
});
|
|
179
|
+
return pending;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Alias for use() matching AdonisJS convention.
|
|
183
|
+
*/
|
|
184
|
+
async driver(name) {
|
|
185
|
+
return this.use(name);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Stops child processes (ACP agents), disposes pending confirmations and forgets drivers.
|
|
189
|
+
* Call on application shutdown.
|
|
190
|
+
*/
|
|
191
|
+
async shutdown() {
|
|
192
|
+
const languageModels = this.config.languageModels ?? this.lazyLanguageModels;
|
|
193
|
+
this.lazyLanguageModels = undefined;
|
|
194
|
+
const drivers = [...this.drivers.values()];
|
|
195
|
+
this.drivers.clear();
|
|
196
|
+
const settled = await Promise.allSettled(drivers);
|
|
197
|
+
await Promise.allSettled(settled.map(async (entry) => {
|
|
198
|
+
if (entry.status === 'fulfilled')
|
|
199
|
+
await entry.value.shutdown?.();
|
|
200
|
+
}));
|
|
201
|
+
await Promise.allSettled([languageModels?.shutdown()]);
|
|
202
|
+
this.confirmation.dispose();
|
|
203
|
+
}
|
|
204
|
+
get languageModels() {
|
|
205
|
+
return (this.config.languageModels ??
|
|
206
|
+
(this.lazyLanguageModels ??= new LanguageModelFactory({
|
|
207
|
+
logger: this.logger,
|
|
208
|
+
acp: { confirmation: this.confirmation },
|
|
209
|
+
})));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Starts loading catalog pricing for a model in the background (deduplicated).
|
|
213
|
+
*/
|
|
214
|
+
warmPricing(providerName, modelName) {
|
|
215
|
+
if (this.config.catalog?.autoPricing === false)
|
|
216
|
+
return Promise.resolve();
|
|
217
|
+
// Chain models are not configured providers: their provider id is already a catalog id.
|
|
218
|
+
const pConfig = this.config.providers[providerName];
|
|
219
|
+
if (pConfig?.driver === 'acp')
|
|
220
|
+
return Promise.resolve();
|
|
221
|
+
const catalogProviderId = pConfig ? resolveCatalogProviderId(pConfig.driver) : providerName;
|
|
222
|
+
const key = `${providerName}/${modelName}`;
|
|
223
|
+
let warmup = this.pricingWarmups.get(key);
|
|
224
|
+
if (!warmup) {
|
|
225
|
+
warmup = (async () => {
|
|
226
|
+
try {
|
|
227
|
+
const pricing = await this.catalog.getModelPricing(catalogProviderId, modelName);
|
|
228
|
+
if (pricing) {
|
|
229
|
+
this.costTracker.setPricing({ [modelName]: pricing, [key]: pricing });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Non-fatal catalog pricing lookup
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
this.pricingWarmups.set(key, warmup);
|
|
237
|
+
}
|
|
238
|
+
return warmup;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Executes an operation with cascading fallback across the primary and explicitly configured
|
|
242
|
+
* backup providers.
|
|
243
|
+
*
|
|
244
|
+
* - The requested model is used for the primary provider only; fallbacks use `fallbackModels`
|
|
245
|
+
* or their own `defaultModel`.
|
|
246
|
+
* - Only transient failures (429, 5xx, network) fall through to the next provider; bad requests,
|
|
247
|
+
* validation errors and aborts are rethrown immediately, as are failures after tools ran.
|
|
248
|
+
* - The circuit breaker is keyed by `provider:model`.
|
|
249
|
+
*/
|
|
250
|
+
async executeWithFallback(request, operation) {
|
|
251
|
+
if (request.models && request.models.length > 0) {
|
|
252
|
+
return this.executeOverChain(request, request.models, operation);
|
|
253
|
+
}
|
|
254
|
+
let targetProvider = request.provider;
|
|
255
|
+
let targetModel = request.model;
|
|
256
|
+
// 'provider/model' syntax, but only when the prefix really is a configured provider
|
|
257
|
+
if (targetModel && targetModel.includes('/') && !targetProvider) {
|
|
258
|
+
const [head, ...rest] = targetModel.split('/');
|
|
259
|
+
if (Object.hasOwn(this.config.providers, head)) {
|
|
260
|
+
targetProvider = head;
|
|
261
|
+
targetModel = rest.join('/');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const primary = targetProvider ?? this.config.default;
|
|
265
|
+
const cascade = [primary, ...(this.config.fallbackCascade ?? []).filter((p) => p !== primary)];
|
|
266
|
+
const attempts = [];
|
|
267
|
+
for (const [index, providerName] of cascade.entries()) {
|
|
268
|
+
request.abortSignal?.throwIfAborted();
|
|
269
|
+
const pConfig = this.config.providers[providerName];
|
|
270
|
+
if (!pConfig)
|
|
271
|
+
continue;
|
|
272
|
+
const isPrimary = index === 0;
|
|
273
|
+
const modelName = isPrimary
|
|
274
|
+
? (targetModel ?? pConfig.defaultModel ?? 'default')
|
|
275
|
+
: ((this.config.fallbackModels && Object.hasOwn(this.config.fallbackModels, providerName)
|
|
276
|
+
? this.config.fallbackModels[providerName]
|
|
277
|
+
: undefined) ??
|
|
278
|
+
pConfig.defaultModel ??
|
|
279
|
+
'default');
|
|
280
|
+
const breakerKey = `${providerName}:${modelName}`;
|
|
281
|
+
if (!this.circuitBreaker.isAvailable(breakerKey)) {
|
|
282
|
+
this.logger?.warn?.(`[AiManager] Skipping "${breakerKey}" because circuit breaker is OPEN.`);
|
|
283
|
+
attempts.push({
|
|
284
|
+
provider: providerName,
|
|
285
|
+
model: modelName,
|
|
286
|
+
error: new Error('circuit breaker is open'),
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const state = { toolsExecuted: false };
|
|
291
|
+
try {
|
|
292
|
+
const driver = await this.use(providerName);
|
|
293
|
+
const result = await this.circuitBreaker.execute(breakerKey, () => operation({
|
|
294
|
+
getModel: () => driver.getModel(modelName),
|
|
295
|
+
modelName,
|
|
296
|
+
providerName,
|
|
297
|
+
breakerKey,
|
|
298
|
+
state,
|
|
299
|
+
}), (err) => classifyError(err, request.abortSignal).countsAsFailure);
|
|
300
|
+
return { result, provider: providerName, model: modelName };
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
304
|
+
const classification = classifyError(err, request.abortSignal);
|
|
305
|
+
if (classification.kind === 'fatal') {
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
if (state.toolsExecuted) {
|
|
309
|
+
this.logger?.error?.(`[AiManager] "${breakerKey}" failed after executing tools (${error.message}); not retrying on another provider to avoid repeating side effects.`);
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
attempts.push({ provider: providerName, model: modelName, error });
|
|
313
|
+
this.logger?.warn?.(`[AiManager] "${breakerKey}" failed (${classification.reason}: ${error.message}). Attempting next fallback...`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
throw new CascadingFallbackError(attempts);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Executes an operation over an explicit chain of models. The provider cascade is not used; the
|
|
320
|
+
* chain is authoritative and every entry has its own per-credential circuit breaker.
|
|
321
|
+
*/
|
|
322
|
+
async executeOverChain(request, models, operation) {
|
|
323
|
+
request.abortSignal?.throwIfAborted();
|
|
324
|
+
const stringEntry = models.find((m) => typeof m === 'string' && m !== '');
|
|
325
|
+
if (stringEntry !== undefined && !this.config.resolver) {
|
|
326
|
+
throw new ProviderConfigError('model-chain', `a resolver is required to resolve "${stringEntry}"`);
|
|
327
|
+
}
|
|
328
|
+
const { result, model } = await runModelChain(toChainCandidates(models), (attempt) => {
|
|
329
|
+
// `state.toolsExecuted = true` inside prepareTools tells the chain not to fall back.
|
|
330
|
+
const state = {
|
|
331
|
+
get toolsExecuted() {
|
|
332
|
+
return false;
|
|
333
|
+
},
|
|
334
|
+
set toolsExecuted(value) {
|
|
335
|
+
if (value)
|
|
336
|
+
attempt.markToolsExecuted();
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
return operation({
|
|
340
|
+
getModel: () => this.languageModels.create(attempt.model),
|
|
341
|
+
modelName: attempt.model.modelID,
|
|
342
|
+
providerName: attempt.model.providerID,
|
|
343
|
+
breakerKey: attempt.key,
|
|
344
|
+
state,
|
|
345
|
+
});
|
|
346
|
+
}, {
|
|
347
|
+
breaker: this.circuitBreaker,
|
|
348
|
+
resolver: this.config.resolver,
|
|
349
|
+
abortSignal: request.abortSignal,
|
|
350
|
+
onFallback: request.onFallback,
|
|
351
|
+
logger: this.logger,
|
|
352
|
+
});
|
|
353
|
+
return { result, provider: model.providerID, model: model.modelID };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Applies tool permissions and the tool guard, and tracks whether any tool actually ran.
|
|
357
|
+
* `deny` tools are dropped, `ask` tools need a human confirmation (fail-closed without one),
|
|
358
|
+
* and every tool passes through `toolGuard.guard` when provided.
|
|
359
|
+
*/
|
|
360
|
+
prepareTools(options, state) {
|
|
361
|
+
const tools = options.tools;
|
|
362
|
+
if (!tools || Object.keys(tools).length === 0)
|
|
363
|
+
return undefined;
|
|
364
|
+
const permissions = options.toolPermissions;
|
|
365
|
+
const guardOptions = options.toolGuard;
|
|
366
|
+
const prepared = {};
|
|
367
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
368
|
+
const permission = permissions && Object.hasOwn(permissions, name) ? permissions[name] : 'allow';
|
|
369
|
+
if (permission === 'deny')
|
|
370
|
+
continue;
|
|
371
|
+
let candidate = tool;
|
|
372
|
+
const original = candidate.execute?.bind(candidate);
|
|
373
|
+
if (original) {
|
|
374
|
+
candidate = {
|
|
375
|
+
...candidate,
|
|
376
|
+
execute: (args, execOptions) => {
|
|
377
|
+
state.toolsExecuted = true;
|
|
378
|
+
return original(args, execOptions);
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (permission === 'ask' || guardOptions?.guard) {
|
|
383
|
+
candidate = this.toolRegistry.wrapTool(name, candidate, {
|
|
384
|
+
permission,
|
|
385
|
+
guard: guardOptions?.guard,
|
|
386
|
+
confirmation: guardOptions?.confirmation ?? this.confirmation,
|
|
387
|
+
sessionId: guardOptions?.sessionId,
|
|
388
|
+
timeoutMs: guardOptions?.timeoutMs,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
prepared[name] = candidate;
|
|
392
|
+
}
|
|
393
|
+
return Object.keys(prepared).length > 0 ? prepared : undefined;
|
|
394
|
+
}
|
|
395
|
+
buildTextParams(options, model, state, signal) {
|
|
396
|
+
const maxOutputTokens = options.maxOutputTokens ?? options.maxTokens;
|
|
397
|
+
const common = {
|
|
398
|
+
model,
|
|
399
|
+
instructions: options.system || undefined,
|
|
400
|
+
temperature: options.temperature,
|
|
401
|
+
topP: options.topP,
|
|
402
|
+
maxOutputTokens,
|
|
403
|
+
tools: this.prepareTools(options, state),
|
|
404
|
+
stopWhen: options.stopWhen ?? (options.maxSteps ? stepCountIs(options.maxSteps) : undefined),
|
|
405
|
+
maxRetries: options.maxRetries,
|
|
406
|
+
onStepFinish: options.onStepFinish
|
|
407
|
+
? async (step) => {
|
|
408
|
+
try {
|
|
409
|
+
await options.onStepFinish?.(step);
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
this.logger?.warn?.(`[AiManager] onStepFinish callback failed: ${String(err)}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
: undefined,
|
|
416
|
+
abortSignal: signal,
|
|
417
|
+
};
|
|
418
|
+
if (options.messages && options.messages.length > 0) {
|
|
419
|
+
return { ...common, messages: options.messages };
|
|
420
|
+
}
|
|
421
|
+
if (options.prompt) {
|
|
422
|
+
return { ...common, prompt: options.prompt };
|
|
423
|
+
}
|
|
424
|
+
throw new AiException('Either "prompt" or non-empty "messages" is required.', 'E_AI_INVALID_ARGUMENT');
|
|
425
|
+
}
|
|
426
|
+
async recordCost(agentName, provider, model, usage, durationMs, pricingReady, metadata) {
|
|
427
|
+
await Promise.race([pricingReady, sleep(PRICING_WAIT_MS)]);
|
|
428
|
+
try {
|
|
429
|
+
return this.costTracker.record({
|
|
430
|
+
agentName,
|
|
431
|
+
model,
|
|
432
|
+
inputTokens: usage.inputTokens,
|
|
433
|
+
outputTokens: usage.outputTokens,
|
|
434
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
435
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
436
|
+
reasoningTokens: usage.reasoningTokens,
|
|
437
|
+
totalTokens: usage.totalTokens,
|
|
438
|
+
durationMs,
|
|
439
|
+
metadata: { provider, ...metadata },
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Cost tracking is non-fatal
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
warnIfOverBudget(agentName, totalTokens, costUsd) {
|
|
448
|
+
const budget = this.config.budget;
|
|
449
|
+
if (budget?.maxTokensPerRequest && totalTokens > budget.maxTokensPerRequest) {
|
|
450
|
+
this.logger?.warn?.(`[AiManager] Token limit exceeded for ${agentName}: ${totalTokens} > ${budget.maxTokensPerRequest}`);
|
|
451
|
+
}
|
|
452
|
+
if (costUsd !== undefined && budget?.maxCostPerRequestUsd && costUsd > budget.maxCostPerRequestUsd) {
|
|
453
|
+
this.logger?.warn?.(`[AiManager] Cost limit exceeded for ${agentName}: $${costUsd} > $${budget.maxCostPerRequestUsd}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Generates text using the configured LLM provider with fallback, budget accounting, and tool guard.
|
|
458
|
+
*/
|
|
459
|
+
async generateText(options) {
|
|
460
|
+
const startTime = Date.now();
|
|
461
|
+
const agentName = options.agentName ?? this.config.budget?.defaultAgentName ?? 'default-agent';
|
|
462
|
+
const { result, provider, model } = await this.executeWithFallback(options, async ({ getModel, modelName, providerName, state }) => {
|
|
463
|
+
void this.warmPricing(providerName, modelName);
|
|
464
|
+
const languageModel = await getModel();
|
|
465
|
+
const params = this.buildTextParams(options, languageModel, state, options.abortSignal);
|
|
466
|
+
return generateText(options.schema ? { ...params, output: Output.object({ schema: options.schema }) } : params);
|
|
467
|
+
});
|
|
468
|
+
const durationMs = Date.now() - startTime;
|
|
469
|
+
// `totalUsage` sums every step of a tool loop; `usage` is the last step only.
|
|
470
|
+
const usage = normalizeUsage(result.totalUsage ?? result.usage);
|
|
471
|
+
const costUsd = await this.recordCost(agentName, provider, model, usage, durationMs, this.warmPricing(provider, model), { finishReason: result.finishReason, ...options.metadata });
|
|
472
|
+
this.warnIfOverBudget(agentName, usage.totalTokens, costUsd);
|
|
473
|
+
return {
|
|
474
|
+
text: result.text,
|
|
475
|
+
usage,
|
|
476
|
+
costUsd,
|
|
477
|
+
finishReason: result.finishReason ?? 'stop',
|
|
478
|
+
provider,
|
|
479
|
+
model,
|
|
480
|
+
toolCalls: result.toolCalls,
|
|
481
|
+
toolResults: result.toolResults,
|
|
482
|
+
steps: result.steps,
|
|
483
|
+
// `output` throws when no output was specified, so read it only for schema calls.
|
|
484
|
+
output: options.schema ? result.output : undefined,
|
|
485
|
+
rawResponse: result.response,
|
|
486
|
+
durationMs,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Streams text from LLM. Initial provider failures (429, 5xx, network) are detected before this
|
|
491
|
+
* method returns, so they trigger fallback and the HTTP layer can still send proper headers.
|
|
492
|
+
* Token usage is recorded when the stream ends or is aborted.
|
|
493
|
+
*/
|
|
494
|
+
async streamText(options) {
|
|
495
|
+
const startTime = Date.now();
|
|
496
|
+
const agentName = options.agentName ?? this.config.budget?.defaultAgentName ?? 'default-agent';
|
|
497
|
+
const controller = new AbortController();
|
|
498
|
+
const signal = options.abortSignal
|
|
499
|
+
? AbortSignal.any([options.abortSignal, controller.signal])
|
|
500
|
+
: controller.signal;
|
|
501
|
+
let settleUsage = () => { };
|
|
502
|
+
const usagePromise = new Promise((resolve) => {
|
|
503
|
+
settleUsage = resolve;
|
|
504
|
+
});
|
|
505
|
+
const { result: raw, provider, model } = await this.executeWithFallback({ ...options, abortSignal: signal }, async ({ getModel, modelName, providerName, breakerKey, state }) => {
|
|
506
|
+
const pricingReady = this.warmPricing(providerName, modelName);
|
|
507
|
+
const languageModel = await getModel();
|
|
508
|
+
let attemptFailed = false;
|
|
509
|
+
let accounted = false;
|
|
510
|
+
let streamStarted = false;
|
|
511
|
+
const finish = (usage, text, finishReason) => {
|
|
512
|
+
if (attemptFailed || accounted)
|
|
513
|
+
return;
|
|
514
|
+
accounted = true;
|
|
515
|
+
void (async () => {
|
|
516
|
+
let costUsd;
|
|
517
|
+
if (usage.totalTokens > 0) {
|
|
518
|
+
costUsd = await this.recordCost(agentName, providerName, modelName, usage, Date.now() - startTime, pricingReady, { finishReason, ...options.metadata });
|
|
519
|
+
this.warnIfOverBudget(agentName, usage.totalTokens, costUsd);
|
|
520
|
+
}
|
|
521
|
+
settleUsage(usage);
|
|
522
|
+
try {
|
|
523
|
+
options.onFinish?.({ text, usage, costUsd });
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
this.logger?.warn?.(`[AiManager] onFinish callback failed: ${String(err)}`);
|
|
527
|
+
}
|
|
528
|
+
})();
|
|
529
|
+
};
|
|
530
|
+
const params = this.buildTextParams(options, languageModel, state, signal);
|
|
531
|
+
const stream = streamText({
|
|
532
|
+
...params,
|
|
533
|
+
...(options.schema ? { output: Output.object({ schema: options.schema }) } : {}),
|
|
534
|
+
onChunk: ({ chunk }) => {
|
|
535
|
+
if (chunk.type === 'text-delta')
|
|
536
|
+
options.onChunk?.(chunk.text);
|
|
537
|
+
},
|
|
538
|
+
onError: ({ error }) => {
|
|
539
|
+
if (!streamStarted)
|
|
540
|
+
return; // surfaced through the first-part check below
|
|
541
|
+
this.logger?.warn?.(`[AiManager] Stream error on "${breakerKey}": ${String(error)}`);
|
|
542
|
+
if (classifyError(error, signal).countsAsFailure) {
|
|
543
|
+
this.circuitBreaker.recordFailure(breakerKey, error);
|
|
544
|
+
}
|
|
545
|
+
finish(EMPTY_USAGE, '');
|
|
546
|
+
},
|
|
547
|
+
onEnd: (event) => finish(normalizeUsage(event.totalUsage), event.text, event.finishReason),
|
|
548
|
+
onAbort: (event) => finish(sumUsage(event.steps.map((s) => s.usage)), ''),
|
|
549
|
+
});
|
|
550
|
+
// Wait for the first meaningful part so early provider errors reject this attempt.
|
|
551
|
+
const iterator = stream.fullStream[Symbol.asyncIterator]();
|
|
552
|
+
try {
|
|
553
|
+
for (;;) {
|
|
554
|
+
const { value, done } = await iterator.next();
|
|
555
|
+
if (done)
|
|
556
|
+
break;
|
|
557
|
+
if (value.type === 'error')
|
|
558
|
+
throw value.error;
|
|
559
|
+
if (value.type === 'abort') {
|
|
560
|
+
throw Object.assign(new Error('Generation aborted'), { name: 'AbortError' });
|
|
561
|
+
}
|
|
562
|
+
if (value.type === 'start' || value.type === 'start-step')
|
|
563
|
+
continue;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
attemptFailed = true;
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
// Release only our peek branch; other consumers read their own tee of the stream.
|
|
573
|
+
await iterator.return?.();
|
|
574
|
+
}
|
|
575
|
+
streamStarted = true;
|
|
576
|
+
return stream;
|
|
577
|
+
});
|
|
578
|
+
// Without a schema the SDK's `output` rejects; expose `undefined` instead.
|
|
579
|
+
const output = options.schema ? Promise.resolve(raw.output) : Promise.resolve(undefined);
|
|
580
|
+
output.catch(() => { }); // a never-read rejection must not surface as unhandled
|
|
581
|
+
return {
|
|
582
|
+
textStream: raw.textStream,
|
|
583
|
+
fullStream: raw.fullStream,
|
|
584
|
+
provider,
|
|
585
|
+
model,
|
|
586
|
+
usage: usagePromise,
|
|
587
|
+
output,
|
|
588
|
+
toTextStreamResponse: (headers) => raw.toTextStreamResponse({ headers }),
|
|
589
|
+
toUIMessageStreamResponse: (opts) => raw.toUIMessageStreamResponse(opts),
|
|
590
|
+
toDataStreamResponse: (headers) => raw.toUIMessageStreamResponse({ headers }),
|
|
591
|
+
consume: async () => {
|
|
592
|
+
const text = await raw.text;
|
|
593
|
+
const usage = await usagePromise;
|
|
594
|
+
return {
|
|
595
|
+
text,
|
|
596
|
+
usage,
|
|
597
|
+
finishReason: (await raw.finishReason) ?? 'stop',
|
|
598
|
+
provider,
|
|
599
|
+
model,
|
|
600
|
+
durationMs: Date.now() - startTime,
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
abort: (reason) => controller.abort(reason),
|
|
604
|
+
rawStream: raw,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Generates a typed structured object conforming to a Zod schema
|
|
609
|
+
* (via `generateText` with `Output.object`, as `generateObject` is deprecated in AI SDK v7).
|
|
610
|
+
*/
|
|
611
|
+
async generateObject(options) {
|
|
612
|
+
const startTime = Date.now();
|
|
613
|
+
const agentName = options.agentName ?? this.config.budget?.defaultAgentName ?? 'default-agent';
|
|
614
|
+
const { result, provider, model } = await this.executeWithFallback(options, async ({ getModel, modelName, providerName, state }) => {
|
|
615
|
+
void this.warmPricing(providerName, modelName);
|
|
616
|
+
const languageModel = await getModel();
|
|
617
|
+
const params = this.buildTextParams(options, languageModel, state, options.abortSignal);
|
|
618
|
+
return generateText({
|
|
619
|
+
...params,
|
|
620
|
+
output: Output.object({
|
|
621
|
+
schema: options.schema,
|
|
622
|
+
name: options.schemaName,
|
|
623
|
+
description: options.schemaDescription,
|
|
624
|
+
}),
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
const durationMs = Date.now() - startTime;
|
|
628
|
+
const usage = normalizeUsage(result.totalUsage ?? result.usage);
|
|
629
|
+
const costUsd = await this.recordCost(agentName, provider, model, usage, durationMs, this.warmPricing(provider, model), { mode: 'structured_object' });
|
|
630
|
+
return {
|
|
631
|
+
object: result.output,
|
|
632
|
+
usage,
|
|
633
|
+
costUsd,
|
|
634
|
+
provider,
|
|
635
|
+
model,
|
|
636
|
+
durationMs,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Fluent conversation builder (Laravel AI / Prism syntax style).
|
|
641
|
+
*/
|
|
642
|
+
chat() {
|
|
643
|
+
const messages = [];
|
|
644
|
+
let systemPrompt;
|
|
645
|
+
let providerOverride;
|
|
646
|
+
let modelOverride;
|
|
647
|
+
let temperatureVal;
|
|
648
|
+
let agentNameVal;
|
|
649
|
+
let toolsVal;
|
|
650
|
+
const builder = {
|
|
651
|
+
system: (prompt) => {
|
|
652
|
+
systemPrompt = prompt;
|
|
653
|
+
return builder;
|
|
654
|
+
},
|
|
655
|
+
user: (content) => {
|
|
656
|
+
messages.push({ role: 'user', content });
|
|
657
|
+
return builder;
|
|
658
|
+
},
|
|
659
|
+
assistant: (content) => {
|
|
660
|
+
messages.push({ role: 'assistant', content });
|
|
661
|
+
return builder;
|
|
662
|
+
},
|
|
663
|
+
provider: (name) => {
|
|
664
|
+
providerOverride = name;
|
|
665
|
+
return builder;
|
|
666
|
+
},
|
|
667
|
+
model: (name) => {
|
|
668
|
+
modelOverride = name;
|
|
669
|
+
return builder;
|
|
670
|
+
},
|
|
671
|
+
temperature: (val) => {
|
|
672
|
+
temperatureVal = val;
|
|
673
|
+
return builder;
|
|
674
|
+
},
|
|
675
|
+
agent: (name) => {
|
|
676
|
+
agentNameVal = name;
|
|
677
|
+
return builder;
|
|
678
|
+
},
|
|
679
|
+
tools: (tools) => {
|
|
680
|
+
toolsVal = tools;
|
|
681
|
+
return builder;
|
|
682
|
+
},
|
|
683
|
+
generate: () => this.generateText({
|
|
684
|
+
system: systemPrompt,
|
|
685
|
+
messages: messages,
|
|
686
|
+
provider: providerOverride,
|
|
687
|
+
model: modelOverride,
|
|
688
|
+
temperature: temperatureVal,
|
|
689
|
+
agentName: agentNameVal,
|
|
690
|
+
tools: toolsVal,
|
|
691
|
+
}),
|
|
692
|
+
stream: () => this.streamText({
|
|
693
|
+
system: systemPrompt,
|
|
694
|
+
messages: messages,
|
|
695
|
+
provider: providerOverride,
|
|
696
|
+
model: modelOverride,
|
|
697
|
+
temperature: temperatureVal,
|
|
698
|
+
agentName: agentNameVal,
|
|
699
|
+
tools: toolsVal,
|
|
700
|
+
}),
|
|
701
|
+
};
|
|
702
|
+
return builder;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Driver factory for OpenAI, Anthropic, Google/Gemini, Groq, Ollama, ACP, and Custom providers.
|
|
706
|
+
*/
|
|
707
|
+
async createDriver(name, config) {
|
|
708
|
+
switch (config.driver) {
|
|
709
|
+
case 'openai': {
|
|
710
|
+
const { createOpenAI } = await this.safeImport('@ai-sdk/openai', 'OpenAI driver requires package "@ai-sdk/openai"');
|
|
711
|
+
const client = createOpenAI({
|
|
712
|
+
apiKey: config.apiKey ?? process.env.OPENAI_API_KEY,
|
|
713
|
+
organization: config.organization ?? process.env.OPENAI_ORG_ID,
|
|
714
|
+
baseURL: config.baseURL ?? process.env.OPENAI_BASE_URL,
|
|
715
|
+
headers: config.headers,
|
|
716
|
+
});
|
|
717
|
+
const defaultModel = config.defaultModel ?? 'gpt-4o';
|
|
718
|
+
return {
|
|
719
|
+
name,
|
|
720
|
+
defaultModel,
|
|
721
|
+
getModel: (m) => client(m ?? defaultModel),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
case 'anthropic': {
|
|
725
|
+
const { createAnthropic } = await this.safeImport('@ai-sdk/anthropic', 'Anthropic driver requires package "@ai-sdk/anthropic"');
|
|
726
|
+
const client = createAnthropic({
|
|
727
|
+
apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY,
|
|
728
|
+
baseURL: config.baseURL ?? process.env.ANTHROPIC_BASE_URL,
|
|
729
|
+
headers: config.headers,
|
|
730
|
+
});
|
|
731
|
+
const defaultModel = config.defaultModel ?? 'claude-3-7-sonnet-latest';
|
|
732
|
+
return {
|
|
733
|
+
name,
|
|
734
|
+
defaultModel,
|
|
735
|
+
getModel: (m) => client(m ?? defaultModel),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
case 'google':
|
|
739
|
+
case 'gemini': {
|
|
740
|
+
const { createGoogleGenerativeAI } = await this.safeImport('@ai-sdk/google', 'Google/Gemini driver requires package "@ai-sdk/google"');
|
|
741
|
+
const client = createGoogleGenerativeAI({
|
|
742
|
+
apiKey: config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
743
|
+
baseURL: config.baseURL,
|
|
744
|
+
headers: config.headers,
|
|
745
|
+
});
|
|
746
|
+
const defaultModel = config.defaultModel ?? 'gemini-2.5-flash';
|
|
747
|
+
return {
|
|
748
|
+
name,
|
|
749
|
+
defaultModel,
|
|
750
|
+
getModel: (m) => client(m ?? defaultModel),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
case 'groq': {
|
|
754
|
+
const { createGroq } = await this.safeImport('@ai-sdk/groq', 'Groq driver requires package "@ai-sdk/groq"');
|
|
755
|
+
const client = createGroq({
|
|
756
|
+
apiKey: config.apiKey ?? process.env.GROQ_API_KEY,
|
|
757
|
+
baseURL: config.baseURL,
|
|
758
|
+
headers: config.headers,
|
|
759
|
+
});
|
|
760
|
+
const defaultModel = config.defaultModel ?? 'llama-3.3-70b-versatile';
|
|
761
|
+
return {
|
|
762
|
+
name,
|
|
763
|
+
defaultModel,
|
|
764
|
+
getModel: (m) => client(m ?? defaultModel),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
case 'ollama': {
|
|
768
|
+
const { createOpenAICompatible } = await this.safeImport('@ai-sdk/openai-compatible', 'Ollama driver requires package "@ai-sdk/openai-compatible"');
|
|
769
|
+
const baseURL = config.baseURL ?? process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434/v1';
|
|
770
|
+
const client = createOpenAICompatible({
|
|
771
|
+
name: 'ollama',
|
|
772
|
+
baseURL,
|
|
773
|
+
headers: config.headers,
|
|
774
|
+
});
|
|
775
|
+
const defaultModel = config.defaultModel ?? 'llama3.2';
|
|
776
|
+
return {
|
|
777
|
+
name,
|
|
778
|
+
defaultModel,
|
|
779
|
+
getModel: (m) => client(m ?? defaultModel),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
case 'acp': {
|
|
783
|
+
const { createAcpDriver } = await import('./acp/provider.js');
|
|
784
|
+
return createAcpDriver(name, config, {
|
|
785
|
+
logger: this.logger,
|
|
786
|
+
confirmation: this.confirmation,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
case 'custom': {
|
|
790
|
+
const customModel = await config.factory(config);
|
|
791
|
+
const defaultModel = config.defaultModel ?? 'custom';
|
|
792
|
+
return {
|
|
793
|
+
name,
|
|
794
|
+
defaultModel,
|
|
795
|
+
getModel: (m) => typeof customModel === 'function' ? customModel(m ?? defaultModel) : customModel,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
default: {
|
|
799
|
+
const unsupported = config;
|
|
800
|
+
throw new ProviderConfigError(name, `Unsupported driver type: "${unsupported.driver}"`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async safeImport(pkgName, helpMsg) {
|
|
805
|
+
try {
|
|
806
|
+
return (await import(__rewriteRelativeImportExtension(pkgName)));
|
|
807
|
+
}
|
|
808
|
+
catch (e) {
|
|
809
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
810
|
+
throw new AiException(`${helpMsg}. Error: ${message}`, 'E_AI_MISSING_DEPENDENCY', e);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=ai_manager.js.map
|