@nebutra/agents 1.1.0 → 1.1.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/LICENSE +21 -676
- package/README.md +7 -3
- package/dist/{agent-CahnMASx.d.ts → agent-DDGWuUpe.d.ts} +1 -1
- package/{src/env.ts → dist/chunk-BMSL4E4A.js} +23 -47
- package/dist/{chunk-5JZJ5KMC.js → chunk-BZKIMAGK.js} +16 -7
- package/dist/{chunk-NPQECBXL.js → chunk-GRSMUTUS.js} +60 -7
- package/dist/{chunk-B7XWL35G.js → chunk-QYZDHC5A.js} +5 -1
- package/dist/{chunk-RLWM437Q.js → chunk-RNFUEMUB.js} +10 -3
- package/dist/chunk-RWQL4HXC.js +171 -0
- package/dist/{chunk-NVPE5EDI.js → chunk-V6VC2O6Q.js} +4 -3
- package/dist/chunk-VZPQOXWW.js +47 -0
- package/dist/{chunk-5LX742GP.js → chunk-XLBS3XUI.js} +4 -50
- package/dist/env.d.ts +42 -0
- package/dist/env.js +12 -0
- package/dist/fallback.d.ts +100 -0
- package/dist/fallback.js +18 -0
- package/dist/generation/index.d.ts +122 -0
- package/dist/generation/index.js +19 -0
- package/dist/index.d.ts +21 -328
- package/dist/index.js +57 -245
- package/dist/observability.d.ts +46 -0
- package/dist/observability.js +13 -0
- package/dist/providers/langchain.d.ts +2 -2
- package/dist/providers/vercel-ai.d.ts +2 -2
- package/dist/providers/vercel-ai.js +15 -7
- package/dist/sdk/config.d.ts +3 -0
- package/dist/sdk/config.js +1 -1
- package/dist/sdk/index.d.ts +36 -3
- package/dist/sdk/index.js +20 -7
- package/dist/sdk/models.d.ts +18 -6
- package/dist/sdk/models.js +1 -1
- package/dist/sdk/provider.d.ts +1 -0
- package/dist/sdk/provider.js +3 -3
- package/dist/tools.d.ts +1 -1
- package/dist/{types-NtgB3pch.d.ts → types-BytC-HfQ.d.ts} +1 -5
- package/package.json +73 -27
- package/.turbo/turbo-build.log +0 -40
- package/.turbo/turbo-test.log +0 -18
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -63
- package/CHANGELOG.md +0 -26
- package/src/__tests__/cost-observability.test.ts +0 -172
- package/src/__tests__/fallback-wiring.test.ts +0 -313
- package/src/__tests__/generation.test.ts +0 -111
- package/src/__tests__/public-api.test.ts +0 -114
- package/src/agent.ts +0 -117
- package/src/context.ts +0 -99
- package/src/fallback.ts +0 -358
- package/src/generation/index.ts +0 -157
- package/src/generation/mock-provider.ts +0 -123
- package/src/generation/types.ts +0 -87
- package/src/index.ts +0 -104
- package/src/memory.ts +0 -126
- package/src/observability.ts +0 -102
- package/src/orchestrator.ts +0 -147
- package/src/providers/langchain.ts +0 -28
- package/src/providers/vercel-ai.ts +0 -114
- package/src/router.ts +0 -158
- package/src/sdk/config.ts +0 -73
- package/src/sdk/index.ts +0 -214
- package/src/sdk/models.ts +0 -57
- package/src/sdk/provider.ts +0 -80
- package/src/tenant.ts +0 -52
- package/src/tools.ts +0 -65
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -21
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LangChain.js agent adapter — stub.
|
|
3
|
-
*
|
|
4
|
-
* This module intentionally throws at construction time to guide
|
|
5
|
-
* developers through the required dependency installation.
|
|
6
|
-
*
|
|
7
|
-
* Once the packages are installed, implement the `execute()` method
|
|
8
|
-
* using LangChain's AgentExecutor or the newer LangGraph approach.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { BaseAgent } from "../agent";
|
|
12
|
-
import type { AgentConfig } from "../types";
|
|
13
|
-
|
|
14
|
-
export class LangChainAgent extends BaseAgent {
|
|
15
|
-
constructor(config: AgentConfig) {
|
|
16
|
-
super(config);
|
|
17
|
-
throw new Error(
|
|
18
|
-
[
|
|
19
|
-
"LangChain agent requires additional packages. Install:",
|
|
20
|
-
"",
|
|
21
|
-
" pnpm add langchain @langchain/core @langchain/openai",
|
|
22
|
-
"",
|
|
23
|
-
"Then implement the execute() method in this file using",
|
|
24
|
-
"LangChain's AgentExecutor or LangGraph.",
|
|
25
|
-
].join("\n"),
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vercel AI SDK agent adapter.
|
|
3
|
-
*
|
|
4
|
-
* Uses `streamText` from the `ai` package with tool-loop support.
|
|
5
|
-
* The `ai` peer dependency is dynamically imported so the package
|
|
6
|
-
* doesn't fail at require-time when the SDK is absent.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { BaseAgent } from "../agent";
|
|
10
|
-
import { runWithFallback, withAnthropicCacheControl } from "../fallback";
|
|
11
|
-
import { buildTelemetryConfig } from "../observability";
|
|
12
|
-
import type { AgentContext, AgentMessage, AgentResponse } from "../types";
|
|
13
|
-
|
|
14
|
-
export class VercelAIAgent extends BaseAgent {
|
|
15
|
-
protected override async execute(
|
|
16
|
-
messages: readonly AgentMessage[],
|
|
17
|
-
context: AgentContext,
|
|
18
|
-
): Promise<AgentResponse> {
|
|
19
|
-
// Dynamic import — avoids hard dependency on `ai`
|
|
20
|
-
const { streamText, stepCountIs, dynamicTool } = await import("ai");
|
|
21
|
-
|
|
22
|
-
type StreamTextParams = Parameters<typeof streamText>[0];
|
|
23
|
-
|
|
24
|
-
// Build AI SDK tool definitions from our AgentTool interface.
|
|
25
|
-
// We use dynamicTool() because our AgentTool uses a loose JSON Schema
|
|
26
|
-
// record type rather than a typed Zod schema.
|
|
27
|
-
const toolSet: StreamTextParams["tools"] = this.config.tools
|
|
28
|
-
? Object.fromEntries(
|
|
29
|
-
this.config.tools.map((t) => [
|
|
30
|
-
t.name,
|
|
31
|
-
dynamicTool({
|
|
32
|
-
description: t.description,
|
|
33
|
-
inputSchema: t.inputSchema as Parameters<typeof dynamicTool>[0]["inputSchema"],
|
|
34
|
-
execute: async (args) => t.execute(args, context),
|
|
35
|
-
}),
|
|
36
|
-
]),
|
|
37
|
-
)
|
|
38
|
-
: undefined;
|
|
39
|
-
|
|
40
|
-
// Build streamText options, conditionally including tools to satisfy
|
|
41
|
-
// exactOptionalPropertyTypes (tools must not be `undefined`).
|
|
42
|
-
//
|
|
43
|
-
// Cost optimization: stable content (system prompt + tool defs) is placed
|
|
44
|
-
// FIRST and dynamic content (user messages) LAST. This ordering is required
|
|
45
|
-
// for both Anthropic explicit prompt caching (90% discount via `cacheControl`)
|
|
46
|
-
// and OpenAI automatic caching (≥1024 token stable prefix). Reordering or
|
|
47
|
-
// mutating the system prompt invalidates the cache on every call.
|
|
48
|
-
const telemetry = buildTelemetryConfig({
|
|
49
|
-
functionId: `agent.${this.config.id}`,
|
|
50
|
-
metadata: {
|
|
51
|
-
tenantId: context.tenantId,
|
|
52
|
-
userId: context.userId,
|
|
53
|
-
sessionId: context.conversationId,
|
|
54
|
-
agentId: this.config.id,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const baseOptionsWithoutModel = {
|
|
59
|
-
system: this.config.instructions,
|
|
60
|
-
messages: messages.map((m) => ({
|
|
61
|
-
role: m.role as "user" | "assistant" | "system",
|
|
62
|
-
content: m.content,
|
|
63
|
-
})),
|
|
64
|
-
stopWhen: stepCountIs(this.config.maxSteps ?? 20),
|
|
65
|
-
// Anthropic prompt cache control on the system message — 90% cost
|
|
66
|
-
// reduction on cached prefix tokens. No-op for non-Anthropic providers.
|
|
67
|
-
providerOptions: withAnthropicCacheControl(),
|
|
68
|
-
experimental_telemetry: telemetry,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Run through the multi-provider fallback chain. The chain is filtered
|
|
72
|
-
// to providers with API keys present (so single-provider deploys are
|
|
73
|
-
// backward-compatible — only the configured provider is tried).
|
|
74
|
-
const { result } = await runWithFallback(
|
|
75
|
-
async (model) => {
|
|
76
|
-
const streamOptions = {
|
|
77
|
-
...baseOptionsWithoutModel,
|
|
78
|
-
model,
|
|
79
|
-
...(toolSet !== undefined ? { tools: toolSet } : {}),
|
|
80
|
-
} as StreamTextParams;
|
|
81
|
-
const r = streamText(streamOptions);
|
|
82
|
-
// Materialize the result so retryable errors surface inside the
|
|
83
|
-
// try/catch in runWithFallback() rather than escaping as unhandled
|
|
84
|
-
// rejections from the lazy stream.
|
|
85
|
-
const text = await r.text;
|
|
86
|
-
const usage = await r.usage;
|
|
87
|
-
return { text, usage };
|
|
88
|
-
},
|
|
89
|
-
{ model: this.config.model },
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const { text, usage } = result;
|
|
93
|
-
|
|
94
|
-
// AI SDK v6 uses inputTokens/outputTokens on LanguageModelUsage
|
|
95
|
-
const inputTokens = usage?.inputTokens ?? 0;
|
|
96
|
-
const outputTokens = usage?.outputTokens ?? 0;
|
|
97
|
-
|
|
98
|
-
const responseMessages: AgentMessage[] = [
|
|
99
|
-
...messages,
|
|
100
|
-
{ role: "assistant" as const, content: text, timestamp: new Date() },
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
messages: responseMessages,
|
|
105
|
-
usage: {
|
|
106
|
-
promptTokens: inputTokens,
|
|
107
|
-
completionTokens: outputTokens,
|
|
108
|
-
totalTokens: inputTokens + outputTokens,
|
|
109
|
-
},
|
|
110
|
-
finishReason: "stop",
|
|
111
|
-
agentId: this.config.id,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
package/src/router.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentRouter — routes incoming messages to the appropriate agent.
|
|
3
|
-
*
|
|
4
|
-
* Strategies:
|
|
5
|
-
* - keyword: fast, pattern-based matching against agent descriptions
|
|
6
|
-
* - llm: uses a small model to classify intent (requires AI SDK)
|
|
7
|
-
* - custom: user-supplied routing function
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { logger } from "@nebutra/logger";
|
|
11
|
-
import type { AgentConfig, AgentContext, RouterConfig } from "./types";
|
|
12
|
-
|
|
13
|
-
export class AgentRouter {
|
|
14
|
-
private readonly config: RouterConfig;
|
|
15
|
-
|
|
16
|
-
constructor(config: RouterConfig) {
|
|
17
|
-
this.config = config;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Route a message to the best-matching agent.
|
|
22
|
-
* Returns the agent ID.
|
|
23
|
-
*/
|
|
24
|
-
async route(
|
|
25
|
-
message: string,
|
|
26
|
-
agents: readonly AgentConfig[],
|
|
27
|
-
context: AgentContext,
|
|
28
|
-
defaultAgentId?: string,
|
|
29
|
-
): Promise<string> {
|
|
30
|
-
if (agents.length === 0) {
|
|
31
|
-
throw new Error("No agents configured in the orchestrator");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const fallback = defaultAgentId ?? agents[0]?.id;
|
|
35
|
-
if (!fallback) {
|
|
36
|
-
throw new Error("No agents configured in the orchestrator");
|
|
37
|
-
}
|
|
38
|
-
switch (this.config.strategy) {
|
|
39
|
-
case "keyword":
|
|
40
|
-
return this.routeByKeyword(message, agents, defaultAgentId) ?? fallback;
|
|
41
|
-
case "llm":
|
|
42
|
-
return (await this.routeByLLM(message, agents, defaultAgentId)) ?? fallback;
|
|
43
|
-
case "custom":
|
|
44
|
-
return this.routeByCustom(message, context, defaultAgentId);
|
|
45
|
-
default: {
|
|
46
|
-
const _exhaustive: never = this.config.strategy;
|
|
47
|
-
throw new Error(`Unknown routing strategy: ${String(_exhaustive)}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Keyword-based routing: score each agent by how many words from
|
|
54
|
-
* its name + description appear in the user message.
|
|
55
|
-
*/
|
|
56
|
-
private routeByKeyword(
|
|
57
|
-
message: string,
|
|
58
|
-
agents: readonly AgentConfig[],
|
|
59
|
-
defaultAgentId?: string,
|
|
60
|
-
): string | undefined {
|
|
61
|
-
const messageLower = message.toLowerCase();
|
|
62
|
-
let bestId = defaultAgentId ?? agents[0]?.id;
|
|
63
|
-
let bestScore = 0;
|
|
64
|
-
|
|
65
|
-
for (const agent of agents) {
|
|
66
|
-
const keywords = `${agent.name} ${agent.description}`.toLowerCase().split(/\s+/);
|
|
67
|
-
|
|
68
|
-
let score = 0;
|
|
69
|
-
for (const keyword of keywords) {
|
|
70
|
-
if (keyword.length > 2 && messageLower.includes(keyword)) {
|
|
71
|
-
score += 1;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (score > bestScore) {
|
|
76
|
-
bestScore = score;
|
|
77
|
-
bestId = agent.id;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
logger.info("Router: keyword match", {
|
|
82
|
-
selectedAgentId: bestId,
|
|
83
|
-
score: bestScore,
|
|
84
|
-
});
|
|
85
|
-
return bestId;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* LLM-based routing: uses a small model to classify the user's intent
|
|
90
|
-
* and select the best agent. Falls back to keyword if AI SDK unavailable.
|
|
91
|
-
*/
|
|
92
|
-
private async routeByLLM(
|
|
93
|
-
message: string,
|
|
94
|
-
agents: readonly AgentConfig[],
|
|
95
|
-
defaultAgentId?: string,
|
|
96
|
-
): Promise<string | undefined> {
|
|
97
|
-
try {
|
|
98
|
-
const { generateText } = await import("ai");
|
|
99
|
-
|
|
100
|
-
const agentList = agents.map((a) => `- ${a.id}: ${a.description}`).join("\n");
|
|
101
|
-
|
|
102
|
-
const result = await generateText({
|
|
103
|
-
model: agents[0]?.model as unknown as Parameters<typeof generateText>[0]["model"],
|
|
104
|
-
system: [
|
|
105
|
-
"You are a routing classifier. Given a user message and a list of agents,",
|
|
106
|
-
"respond with ONLY the agent ID that best matches the user's intent.",
|
|
107
|
-
"If no agent is a good match, respond with the default agent ID.",
|
|
108
|
-
"",
|
|
109
|
-
`Available agents:\n${agentList}`,
|
|
110
|
-
"",
|
|
111
|
-
defaultAgentId ? `Default agent: ${defaultAgentId}` : "",
|
|
112
|
-
].join("\n"),
|
|
113
|
-
messages: [{ role: "user" as const, content: message }],
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const selectedId = result.text.trim();
|
|
117
|
-
const validAgent = agents.find((a) => a.id === selectedId);
|
|
118
|
-
|
|
119
|
-
if (validAgent) {
|
|
120
|
-
logger.info("Router: LLM classification", {
|
|
121
|
-
selectedAgentId: validAgent.id,
|
|
122
|
-
});
|
|
123
|
-
return validAgent.id;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// LLM returned an invalid ID — fall back
|
|
127
|
-
return defaultAgentId ?? agents[0]?.id;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
logger.warn("Router: LLM routing failed, falling back to keyword", {
|
|
130
|
-
error,
|
|
131
|
-
});
|
|
132
|
-
return this.routeByKeyword(message, agents, defaultAgentId);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Custom routing via user-supplied function.
|
|
138
|
-
*/
|
|
139
|
-
private async routeByCustom(
|
|
140
|
-
message: string,
|
|
141
|
-
context: AgentContext,
|
|
142
|
-
defaultAgentId?: string,
|
|
143
|
-
): Promise<string> {
|
|
144
|
-
if (!this.config.customRouter) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
'RouterConfig strategy is "custom" but no customRouter function was provided',
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
return await this.config.customRouter(message, context);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
logger.warn("Router: custom router failed", { error });
|
|
154
|
-
if (defaultAgentId) return defaultAgentId;
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
package/src/sdk/config.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime configuration for the Vercel AI SDK-backed helpers
|
|
3
|
-
* (`generateText`, `streamText`, `embed`, `embedMany`).
|
|
4
|
-
*
|
|
5
|
-
* Previously lived in `@nebutra/ai-sdk/config`. Absorbed into
|
|
6
|
-
* `@nebutra/agents` during the AI package consolidation so there is
|
|
7
|
-
* a single AI runtime package.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Supported AI provider backends for the top-level helpers.
|
|
14
|
-
*
|
|
15
|
-
* - openrouter: 300+ models, automatic failover, pay-as-you-go (default)
|
|
16
|
-
* - openai: Direct OpenAI API access
|
|
17
|
-
* - siliconflow: SiliconFlow cloud — Qwen, DeepSeek, etc. (OpenAI-compatible, China-optimized)
|
|
18
|
-
* - gateway: Vercel AI Gateway with OIDC auth (for Vercel-deployed apps)
|
|
19
|
-
*/
|
|
20
|
-
export const ProviderType = z.enum(["openrouter", "openai", "siliconflow", "gateway"]);
|
|
21
|
-
export type ProviderType = z.infer<typeof ProviderType>;
|
|
22
|
-
|
|
23
|
-
export const NebutraAIConfigSchema = z.object({
|
|
24
|
-
/** Which provider backend to use. Defaults to "openrouter". */
|
|
25
|
-
provider: ProviderType.default("openrouter"),
|
|
26
|
-
|
|
27
|
-
/** API key override. Falls back to env vars per provider. */
|
|
28
|
-
apiKey: z.string().optional(),
|
|
29
|
-
|
|
30
|
-
/** Default model identifier. Provider-specific format. */
|
|
31
|
-
defaultModel: z.string().default("anthropic/claude-sonnet-4"),
|
|
32
|
-
|
|
33
|
-
/** Default temperature for generations. */
|
|
34
|
-
temperature: z.number().min(0).max(2).default(0.7),
|
|
35
|
-
|
|
36
|
-
/** Default max tokens for output. */
|
|
37
|
-
maxTokens: z.number().int().positive().optional(),
|
|
38
|
-
|
|
39
|
-
/** Extra headers merged into every request (e.g. HTTP-Referer for OpenRouter). */
|
|
40
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
41
|
-
|
|
42
|
-
/** Extra body fields merged into every request. */
|
|
43
|
-
extraBody: z.record(z.string(), z.unknown()).optional(),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export type NebutraAIConfig = z.input<typeof NebutraAIConfigSchema>;
|
|
47
|
-
export type ResolvedNebutraAIConfig = z.output<typeof NebutraAIConfigSchema>;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Resolves the API key from explicit config or environment variables.
|
|
51
|
-
*/
|
|
52
|
-
export function resolveApiKey(config: ResolvedNebutraAIConfig): string {
|
|
53
|
-
if (config.apiKey) return config.apiKey;
|
|
54
|
-
|
|
55
|
-
const envMap: Record<ProviderType, string> = {
|
|
56
|
-
openrouter: "OPENROUTER_API_KEY",
|
|
57
|
-
openai: "OPENAI_API_KEY",
|
|
58
|
-
siliconflow: "SILICONFLOW_API_KEY",
|
|
59
|
-
gateway: "VERCEL_OIDC_TOKEN",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const envVar = envMap[config.provider];
|
|
63
|
-
|
|
64
|
-
const value = globalThis.process?.env?.[envVar];
|
|
65
|
-
|
|
66
|
-
if (!value) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`[@nebutra/agents] Missing API key. Set "${envVar}" in environment or pass "apiKey" in config.`,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return value;
|
|
73
|
-
}
|
package/src/sdk/index.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Top-level Vercel AI SDK helpers — absorbed from the former
|
|
3
|
-
* `@nebutra/ai-sdk` package during the AI consolidation.
|
|
4
|
-
*
|
|
5
|
-
* Public API (unchanged):
|
|
6
|
-
* configure(), getConfig()
|
|
7
|
-
* generateText(), streamText()
|
|
8
|
-
* embed(), embedMany()
|
|
9
|
-
* createModel(), createEmbeddingModel()
|
|
10
|
-
* models, resolveModel()
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
embed as _embed,
|
|
15
|
-
embedMany as _embedMany,
|
|
16
|
-
generateText as _generateText,
|
|
17
|
-
streamText as _streamText,
|
|
18
|
-
type GenerateTextResult,
|
|
19
|
-
type JSONValue,
|
|
20
|
-
type ModelMessage,
|
|
21
|
-
type StreamTextResult,
|
|
22
|
-
} from "ai";
|
|
23
|
-
import { runEmbedWithFallback } from "../fallback";
|
|
24
|
-
import {
|
|
25
|
-
type NebutraAIConfig,
|
|
26
|
-
NebutraAIConfigSchema,
|
|
27
|
-
type ResolvedNebutraAIConfig,
|
|
28
|
-
} from "./config";
|
|
29
|
-
import { createModel } from "./provider";
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Singleton config — call `configure()` once at app startup
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
let _resolved: ResolvedNebutraAIConfig = NebutraAIConfigSchema.parse({});
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Initialise the global Nebutra AI configuration.
|
|
39
|
-
* Call once in your app entry point (e.g. instrumentation.ts or layout.tsx).
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```ts
|
|
43
|
-
* import { configure } from "@nebutra/agents";
|
|
44
|
-
*
|
|
45
|
-
* configure({ provider: "openrouter" });
|
|
46
|
-
* // → reads OPENROUTER_API_KEY from env automatically
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export function configure(config: NebutraAIConfig = {}): void {
|
|
50
|
-
_resolved = NebutraAIConfigSchema.parse(config);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Returns the current resolved config (read-only). */
|
|
54
|
-
export function getConfig(): Readonly<ResolvedNebutraAIConfig> {
|
|
55
|
-
return _resolved;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Core generation helpers
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Stream-completion event passed to {@link StreamOptions.onFinish}.
|
|
64
|
-
* Mirrors the AI SDK's `streamText.onFinish` event shape but is decoupled
|
|
65
|
-
* from the SDK internals so consumers don't break on SDK version bumps.
|
|
66
|
-
*/
|
|
67
|
-
export interface StreamFinishEvent {
|
|
68
|
-
/** Final accumulated text of the model's response. */
|
|
69
|
-
text: string;
|
|
70
|
-
/** Reason the stream terminated (e.g. "stop", "length", "tool-calls"). */
|
|
71
|
-
finishReason: string;
|
|
72
|
-
/** Token usage for the request (best-effort; provider-dependent). */
|
|
73
|
-
usage: {
|
|
74
|
-
inputTokens: number | undefined;
|
|
75
|
-
outputTokens: number | undefined;
|
|
76
|
-
totalTokens: number | undefined;
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface GenerateOptions {
|
|
81
|
-
/** Model ID or preset alias (e.g. "flagship", "fast", "anthropic/claude-sonnet-4"). */
|
|
82
|
-
model?: string;
|
|
83
|
-
/** System prompt prepended to the conversation. */
|
|
84
|
-
system?: string;
|
|
85
|
-
temperature?: number;
|
|
86
|
-
maxTokens?: number;
|
|
87
|
-
/** OpenRouter-specific provider options (reasoning, cacheControl, etc.). */
|
|
88
|
-
providerOptions?: Record<string, JSONValue | undefined>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Options accepted by {@link streamText}. Adds the durable `onFinish` hook on
|
|
93
|
-
* top of {@link GenerateOptions}.
|
|
94
|
-
*/
|
|
95
|
-
export interface StreamOptions extends GenerateOptions {
|
|
96
|
-
/**
|
|
97
|
-
* Invoked once the model has finished streaming, regardless of whether the
|
|
98
|
-
* client kept the response connection open. Use this for server-side
|
|
99
|
-
* persistence (e.g. saving chat sessions to a database) — it is the durable
|
|
100
|
-
* hook for write-once side-effects.
|
|
101
|
-
*
|
|
102
|
-
* Errors thrown inside the callback are caught by the AI SDK and logged;
|
|
103
|
-
* they do not surface to the streaming client.
|
|
104
|
-
*/
|
|
105
|
-
onFinish?: (event: StreamFinishEvent) => void | Promise<void>;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Generate a complete text response.
|
|
110
|
-
*/
|
|
111
|
-
export async function generateText(
|
|
112
|
-
messages: ModelMessage[],
|
|
113
|
-
options: GenerateOptions = {},
|
|
114
|
-
): Promise<GenerateTextResult<Record<string, never>, never>> {
|
|
115
|
-
const model = createModel(options.model ?? _resolved.defaultModel, _resolved);
|
|
116
|
-
|
|
117
|
-
return await _generateText({
|
|
118
|
-
model,
|
|
119
|
-
messages,
|
|
120
|
-
...(options.system ? { system: options.system } : {}),
|
|
121
|
-
temperature: options.temperature ?? _resolved.temperature,
|
|
122
|
-
...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
|
|
123
|
-
...(options.providerOptions
|
|
124
|
-
? { providerOptions: { openrouter: options.providerOptions } }
|
|
125
|
-
: {}),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Stream a text response for real-time UI.
|
|
131
|
-
*/
|
|
132
|
-
export async function streamText(
|
|
133
|
-
messages: ModelMessage[],
|
|
134
|
-
options: StreamOptions = {},
|
|
135
|
-
): Promise<StreamTextResult<Record<string, never>, never>> {
|
|
136
|
-
const model = createModel(options.model ?? _resolved.defaultModel, _resolved);
|
|
137
|
-
const userOnFinish = options.onFinish;
|
|
138
|
-
|
|
139
|
-
return _streamText({
|
|
140
|
-
model,
|
|
141
|
-
messages,
|
|
142
|
-
...(options.system ? { system: options.system } : {}),
|
|
143
|
-
temperature: options.temperature ?? _resolved.temperature,
|
|
144
|
-
...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
|
|
145
|
-
...(options.providerOptions
|
|
146
|
-
? { providerOptions: { openrouter: options.providerOptions } }
|
|
147
|
-
: {}),
|
|
148
|
-
...(userOnFinish
|
|
149
|
-
? {
|
|
150
|
-
onFinish: ({ text, finishReason, totalUsage }) => {
|
|
151
|
-
const event: StreamFinishEvent = {
|
|
152
|
-
text,
|
|
153
|
-
finishReason: String(finishReason),
|
|
154
|
-
usage: {
|
|
155
|
-
inputTokens: totalUsage?.inputTokens,
|
|
156
|
-
outputTokens: totalUsage?.outputTokens,
|
|
157
|
-
totalTokens: totalUsage?.totalTokens,
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
return userOnFinish(event);
|
|
161
|
-
},
|
|
162
|
-
}
|
|
163
|
-
: {}),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Embeddings
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
export interface EmbedOptions {
|
|
172
|
-
/** Embedding model ID or preset alias. Defaults to "embedding". */
|
|
173
|
-
model?: string;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Generate an embedding vector for a single value.
|
|
178
|
-
*
|
|
179
|
-
* Uses `runEmbedWithFallback()` so retryable failures (429 / 5xx / network)
|
|
180
|
-
* automatically rotate to the next provider in `LLM_EMBEDDING_FALLBACK_CHAIN`.
|
|
181
|
-
*/
|
|
182
|
-
export async function embed(value: string, options: EmbedOptions = {}) {
|
|
183
|
-
const { result } = await runEmbedWithFallback(async (model) => _embed({ model, value }), {
|
|
184
|
-
model: options.model ?? "embedding",
|
|
185
|
-
});
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Generate embedding vectors for multiple values in a single request.
|
|
191
|
-
*
|
|
192
|
-
* Uses `runEmbedWithFallback()` for provider rotation on retryable errors.
|
|
193
|
-
*/
|
|
194
|
-
export async function embedMany(values: string[], options: EmbedOptions = {}) {
|
|
195
|
-
const { result } = await runEmbedWithFallback(async (model) => _embedMany({ model, values }), {
|
|
196
|
-
model: options.model ?? "embedding",
|
|
197
|
-
});
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
// Re-exports
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
|
|
205
|
-
export type { GenerateTextResult, ModelMessage, StreamTextResult } from "ai";
|
|
206
|
-
export {
|
|
207
|
-
type NebutraAIConfig,
|
|
208
|
-
NebutraAIConfigSchema,
|
|
209
|
-
type ProviderType,
|
|
210
|
-
type ResolvedNebutraAIConfig,
|
|
211
|
-
} from "./config";
|
|
212
|
-
export type { ModelPreset } from "./models";
|
|
213
|
-
export { models, resolveModel } from "./models";
|
|
214
|
-
export { createEmbeddingModel, createModel } from "./provider";
|
package/src/sdk/models.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model presets for common Nebutra use cases.
|
|
3
|
-
*
|
|
4
|
-
* All IDs use "vendor/model" format (OpenRouter / SiliconFlow).
|
|
5
|
-
* When using direct OpenAI provider, only OpenAI models are valid.
|
|
6
|
-
* When using Vercel AI Gateway, use "provider/model" format.
|
|
7
|
-
* When using SiliconFlow, use "Vendor/Model" format (e.g. "Qwen/Qwen2.5-72B-Instruct").
|
|
8
|
-
*/
|
|
9
|
-
export const models = {
|
|
10
|
-
/** High-quality reasoning — default for complex tasks */
|
|
11
|
-
flagship: "anthropic/claude-sonnet-4",
|
|
12
|
-
|
|
13
|
-
/** Deep reasoning for architecture and research */
|
|
14
|
-
reasoning: "anthropic/claude-opus-4",
|
|
15
|
-
|
|
16
|
-
/** Fast + cheap — chat, summaries, classification */
|
|
17
|
-
fast: "anthropic/claude-haiku-4",
|
|
18
|
-
|
|
19
|
-
/** OpenAI flagship */
|
|
20
|
-
"openai-flagship": "openai/gpt-5.4",
|
|
21
|
-
|
|
22
|
-
/** Google flagship */
|
|
23
|
-
"google-flagship": "google/gemini-2.5-pro",
|
|
24
|
-
|
|
25
|
-
/** Google fast */
|
|
26
|
-
"google-fast": "google/gemini-2.5-flash",
|
|
27
|
-
|
|
28
|
-
/** Embedding model */
|
|
29
|
-
embedding: "openai/text-embedding-3-small",
|
|
30
|
-
|
|
31
|
-
/** Embedding model (high-dimensional) */
|
|
32
|
-
"embedding-large": "openai/text-embedding-3-large",
|
|
33
|
-
|
|
34
|
-
// --- SiliconFlow presets (use with provider: "siliconflow") ---
|
|
35
|
-
|
|
36
|
-
/** SiliconFlow — Qwen 2.5 72B (flagship open-source) */
|
|
37
|
-
"sf-qwen": "Qwen/Qwen2.5-72B-Instruct",
|
|
38
|
-
|
|
39
|
-
/** SiliconFlow — DeepSeek R1 reasoning (via SiliconFlow Pro) */
|
|
40
|
-
"sf-deepseek-r1": "Pro/deepseek-ai/DeepSeek-R1",
|
|
41
|
-
|
|
42
|
-
/** SiliconFlow — DeepSeek V3 (fast, capable) */
|
|
43
|
-
"sf-deepseek-v3": "deepseek-ai/DeepSeek-V3",
|
|
44
|
-
} as const;
|
|
45
|
-
|
|
46
|
-
export type ModelPreset = keyof typeof models;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resolves a model preset alias to its full model ID.
|
|
50
|
-
* If the input is not a preset key, returns it as-is (passthrough).
|
|
51
|
-
*/
|
|
52
|
-
export function resolveModel(modelOrPreset: string): string {
|
|
53
|
-
if (modelOrPreset in models) {
|
|
54
|
-
return models[modelOrPreset as ModelPreset];
|
|
55
|
-
}
|
|
56
|
-
return modelOrPreset;
|
|
57
|
-
}
|