@nebutra/agents 1.1.1 → 2.0.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/LICENSE +21 -676
- package/README.md +7 -3
- package/dist/{agent-CahnMASx.d.ts → agent-DDGWuUpe.d.ts} +1 -1
- package/dist/{chunk-B7XWL35G.js → chunk-7VL333VJ.js} +5 -1
- package/dist/{chunk-5LX742GP.js → chunk-C4CC5UEF.js} +25 -53
- package/{src/env.ts → dist/chunk-FCXWOXII.js} +23 -47
- package/dist/chunk-HVLFZW6E.js +71 -0
- package/dist/chunk-HZQXXUKB.js +171 -0
- package/dist/{chunk-RLWM437Q.js → chunk-KC6SOI5Z.js} +19 -4
- package/dist/{chunk-NPQECBXL.js → chunk-S5N743EP.js} +60 -7
- package/dist/chunk-VZPQOXWW.js +47 -0
- package/dist/{chunk-NVPE5EDI.js → chunk-YBCIJKC7.js} +20 -3
- package/dist/env.d.ts +45 -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 +22 -329
- package/dist/index.js +59 -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 +6 -0
- package/dist/sdk/config.js +2 -1
- package/dist/sdk/index.d.ts +37 -4
- package/dist/sdk/index.js +23 -8
- package/dist/sdk/models.d.ts +20 -27
- package/dist/sdk/models.js +5 -3
- 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 -19
- package/.turbo/turbo-build.log +0 -40
- package/.turbo/turbo-test.log +0 -19
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -63
- package/CHANGELOG.md +0 -36
- package/dist/chunk-5JZJ5KMC.js +0 -37
- 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/__tests__/runtime-gateway.test.ts +0 -108
- package/src/agent.ts +0 -117
- package/src/context.ts +0 -99
- package/src/fallback.ts +0 -358
- package/src/gateway.ts +0 -234
- 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
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
|
-
}
|
package/src/sdk/provider.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
-
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
3
|
-
import type { EmbeddingModel, LanguageModel } from "ai";
|
|
4
|
-
import { type ResolvedNebutraAIConfig, resolveApiKey } from "./config";
|
|
5
|
-
import { resolveModel } from "./models";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a language model instance based on the resolved config.
|
|
9
|
-
*
|
|
10
|
-
* Provider routing:
|
|
11
|
-
* - "openrouter" → @openrouter/ai-sdk-provider (300+ models, failover)
|
|
12
|
-
* - "openai" → @ai-sdk/openai (direct OpenAI API)
|
|
13
|
-
* - "siliconflow" → @ai-sdk/openai with SiliconFlow baseURL (OpenAI-compatible)
|
|
14
|
-
* - "gateway" → Vercel AI Gateway via plain model string (OIDC auth)
|
|
15
|
-
*/
|
|
16
|
-
export function createModel(modelOrPreset: string, config: ResolvedNebutraAIConfig): LanguageModel {
|
|
17
|
-
const modelId = resolveModel(modelOrPreset);
|
|
18
|
-
const apiKey = resolveApiKey(config);
|
|
19
|
-
|
|
20
|
-
switch (config.provider) {
|
|
21
|
-
case "openrouter": {
|
|
22
|
-
const provider = createOpenRouter({
|
|
23
|
-
apiKey,
|
|
24
|
-
...(config.headers ? { headers: config.headers } : {}),
|
|
25
|
-
...(config.extraBody ? { extraBody: config.extraBody } : {}),
|
|
26
|
-
});
|
|
27
|
-
return provider.chat(modelId);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
case "openai": {
|
|
31
|
-
const provider = createOpenAI({ apiKey });
|
|
32
|
-
return provider(modelId);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
case "siliconflow": {
|
|
36
|
-
// SiliconFlow: OpenAI-compatible API with China-optimized infra
|
|
37
|
-
// Models use "Vendor/Model" format, e.g. "Qwen/Qwen2.5-72B-Instruct"
|
|
38
|
-
const provider = createOpenAI({
|
|
39
|
-
apiKey,
|
|
40
|
-
baseURL: "https://api.siliconflow.cn/v1",
|
|
41
|
-
});
|
|
42
|
-
return provider(modelId);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
case "gateway": {
|
|
46
|
-
// Vercel AI Gateway: use @ai-sdk/openai with gateway baseURL
|
|
47
|
-
// OIDC token is passed as apiKey, routed through Vercel's proxy
|
|
48
|
-
const provider = createOpenAI({
|
|
49
|
-
apiKey,
|
|
50
|
-
baseURL: "https://ai-gateway.vercel.sh/v1",
|
|
51
|
-
});
|
|
52
|
-
return provider(modelId);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Creates an embedding model instance for vector operations.
|
|
59
|
-
* Only supported with OpenRouter provider.
|
|
60
|
-
*/
|
|
61
|
-
export function createEmbeddingModel(
|
|
62
|
-
modelOrPreset: string,
|
|
63
|
-
config: ResolvedNebutraAIConfig,
|
|
64
|
-
): EmbeddingModel {
|
|
65
|
-
const modelId = resolveModel(modelOrPreset);
|
|
66
|
-
const apiKey = resolveApiKey(config);
|
|
67
|
-
|
|
68
|
-
if (config.provider !== "openrouter") {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`[@nebutra/agents] Embedding models are currently only supported with the "openrouter" provider.`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const provider = createOpenRouter({
|
|
75
|
-
apiKey,
|
|
76
|
-
...(config.headers ? { headers: config.headers } : {}),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return provider.textEmbeddingModel(modelId);
|
|
80
|
-
}
|
package/src/tenant.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tenant-scoped agent execution context.
|
|
3
|
-
*
|
|
4
|
-
* Ensures every agent operation carries tenantId for RLS,
|
|
5
|
-
* billing, and audit trail purposes.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { AgentContext } from "./types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create a fully-populated AgentContext.
|
|
12
|
-
* Generates a random conversationId when none is provided.
|
|
13
|
-
*/
|
|
14
|
-
export function createAgentContext(
|
|
15
|
-
tenantId: string,
|
|
16
|
-
userId: string,
|
|
17
|
-
conversationId?: string,
|
|
18
|
-
metadata?: Record<string, unknown>,
|
|
19
|
-
): AgentContext {
|
|
20
|
-
return {
|
|
21
|
-
tenantId,
|
|
22
|
-
userId,
|
|
23
|
-
conversationId: conversationId ?? crypto.randomUUID(),
|
|
24
|
-
...(metadata !== undefined ? { metadata } : {}),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Validate that a tenant has remaining quota for agent execution.
|
|
30
|
-
*
|
|
31
|
-
* Returns `{ allowed: true, remaining: -1 }` (unlimited) by default.
|
|
32
|
-
* Integrate with @nebutra/billing entitlements for production usage.
|
|
33
|
-
*/
|
|
34
|
-
export async function checkAgentQuota(
|
|
35
|
-
tenantId: string,
|
|
36
|
-
): Promise<{ allowed: boolean; remaining: number }> {
|
|
37
|
-
try {
|
|
38
|
-
const { getCreditBalance } = await import("@nebutra/billing/credits");
|
|
39
|
-
const balance = await getCreditBalance(tenantId);
|
|
40
|
-
|
|
41
|
-
// Simple quota check: tenant must have positive credits
|
|
42
|
-
// In production, we might check plan-specific monthly limits beforehand
|
|
43
|
-
if (balance.balance > 0) {
|
|
44
|
-
return { allowed: true, remaining: balance.balance };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { allowed: false, remaining: 0 };
|
|
48
|
-
} catch (_err) {
|
|
49
|
-
// Failsafe open if billing is unconfigured
|
|
50
|
-
return { allowed: true, remaining: -1 };
|
|
51
|
-
}
|
|
52
|
-
}
|
package/src/tools.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in tool registry for agents.
|
|
3
|
-
*
|
|
4
|
-
* These are safe, tenant-scoped stubs that demonstrate the tool interface.
|
|
5
|
-
* Each tool requires configuration (API keys, vector stores, etc.) before
|
|
6
|
-
* it produces real results.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { AgentTool } from "./types";
|
|
10
|
-
|
|
11
|
-
/** Search the web for current information. */
|
|
12
|
-
export const webSearchTool: AgentTool = {
|
|
13
|
-
name: "web_search",
|
|
14
|
-
description: "Search the web for current information",
|
|
15
|
-
inputSchema: {
|
|
16
|
-
type: "object",
|
|
17
|
-
properties: { query: { type: "string" } },
|
|
18
|
-
required: ["query"],
|
|
19
|
-
},
|
|
20
|
-
execute: async (_input, _context) => {
|
|
21
|
-
return {
|
|
22
|
-
results: [],
|
|
23
|
-
note: "Configure TAVILY_API_KEY or SERPER_API_KEY for live search",
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** Query the tenant's database (tenant-scoped via RLS). */
|
|
29
|
-
export const databaseQueryTool: AgentTool = {
|
|
30
|
-
name: "database_query",
|
|
31
|
-
description: "Query the tenant's database",
|
|
32
|
-
inputSchema: {
|
|
33
|
-
type: "object",
|
|
34
|
-
properties: { query: { type: "string" } },
|
|
35
|
-
required: ["query"],
|
|
36
|
-
},
|
|
37
|
-
execute: async (_input, context) => {
|
|
38
|
-
return {
|
|
39
|
-
note: `Query would execute in tenant ${context.tenantId} scope`,
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** RAG-style knowledge base retrieval. */
|
|
45
|
-
export const knowledgeBaseTool: AgentTool = {
|
|
46
|
-
name: "knowledge_base",
|
|
47
|
-
description: "Search the organization's knowledge base for relevant documents",
|
|
48
|
-
inputSchema: {
|
|
49
|
-
type: "object",
|
|
50
|
-
properties: {
|
|
51
|
-
query: { type: "string" },
|
|
52
|
-
limit: { type: "number" },
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
execute: async (_input, _context) => {
|
|
56
|
-
return { documents: [], note: "Configure vector store for RAG" };
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** All pre-built tools. */
|
|
61
|
-
export const BUILT_IN_TOOLS: readonly AgentTool[] = [
|
|
62
|
-
webSearchTool,
|
|
63
|
-
databaseQueryTool,
|
|
64
|
-
knowledgeBaseTool,
|
|
65
|
-
];
|