@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
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ integration hook lives here in `providers/langchain.ts` as an extension point.
|
|
|
41
41
|
```ts
|
|
42
42
|
import { configure, streamText } from "@nebutra/agents";
|
|
43
43
|
|
|
44
|
-
configure({ provider: "openrouter", defaultModel: "anthropic/claude-sonnet-4" });
|
|
44
|
+
configure({ provider: "openrouter", defaultModel: "anthropic/claude-sonnet-4.6" });
|
|
45
45
|
|
|
46
46
|
const result = await streamText(
|
|
47
47
|
[{ role: "user", content: "Explain monorepos" }],
|
|
@@ -58,13 +58,13 @@ import { VercelAIAgent } from "@nebutra/agents/providers/vercel-ai";
|
|
|
58
58
|
|
|
59
59
|
const orchestrator = new AgentOrchestrator({
|
|
60
60
|
agents: [
|
|
61
|
-
{ id: "assistant", name: "Assistant", description: "Helpful", model: "openai/gpt-
|
|
61
|
+
{ id: "assistant", name: "Assistant", description: "Helpful", model: "openai/gpt-5.5", instructions: "..." },
|
|
62
62
|
],
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
// Swap the BaseAgent for a real VercelAIAgent at runtime:
|
|
66
66
|
orchestrator.registerAgent(
|
|
67
|
-
new VercelAIAgent({ id: "assistant", name: "Assistant", description: "Helpful", model: "openai/gpt-
|
|
67
|
+
new VercelAIAgent({ id: "assistant", name: "Assistant", description: "Helpful", model: "openai/gpt-5.5", instructions: "..." }),
|
|
68
68
|
);
|
|
69
69
|
|
|
70
70
|
const ctx = createAgentContext("org_123", "user_456");
|
|
@@ -76,3 +76,7 @@ const response = await orchestrator.chat("Hello", ctx);
|
|
|
76
76
|
|
|
77
77
|
Every agent operation requires a `tenantId`. Usage events are emitted for
|
|
78
78
|
billing and metering integration (see `@nebutra/billing/credits`).
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as AgentConfig, A as AgentMessage, a as AgentContext, b as AgentResponse } from './types-
|
|
1
|
+
import { c as AgentConfig, A as AgentMessage, a as AgentContext, b as AgentResponse } from './types-BytC-HfQ.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* BaseAgent — abstract agent with memory, usage tracking, and tenant scoping.
|
|
@@ -1,79 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
* Environment validation for `@nebutra/agents`.
|
|
3
|
-
*
|
|
4
|
-
* All variables are OPTIONAL — the package must work with zero new env config.
|
|
5
|
-
* Provider keys (OPENROUTER_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)
|
|
6
|
-
* are validated lazily by the provider resolver, not here.
|
|
7
|
-
*/
|
|
8
|
-
|
|
1
|
+
// src/env.ts
|
|
9
2
|
import { z } from "zod";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const FallbackChain = z
|
|
16
|
-
.string()
|
|
17
|
-
.transform((raw) =>
|
|
18
|
-
raw
|
|
19
|
-
.split(",")
|
|
20
|
-
.map((s) => s.trim())
|
|
21
|
-
.filter(Boolean),
|
|
22
|
-
)
|
|
23
|
-
.pipe(z.array(FallbackProviderName).min(1));
|
|
24
|
-
|
|
25
|
-
export const AgentsEnvSchema = z.object({
|
|
3
|
+
var FallbackProviderName = z.enum(["openrouter", "anthropic", "openai"]);
|
|
4
|
+
var FallbackChain = z.string().transform(
|
|
5
|
+
(raw) => raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
6
|
+
).pipe(z.array(FallbackProviderName).min(1));
|
|
7
|
+
var AgentsEnvSchema = z.object({
|
|
26
8
|
// ── Anthropic (direct) ─────────────────────────────────────────────────
|
|
27
9
|
ANTHROPIC_API_KEY: z.string().optional(),
|
|
28
|
-
|
|
29
10
|
// ── Langfuse (LLM tracing — optional) ─────────────────────────────────
|
|
30
11
|
LANGFUSE_PUBLIC_KEY: z.string().optional(),
|
|
31
12
|
LANGFUSE_SECRET_KEY: z.string().optional(),
|
|
32
13
|
LANGFUSE_HOST: z.string().url().default("https://cloud.langfuse.com"),
|
|
33
|
-
|
|
34
14
|
// ── Multi-provider fallback chain ─────────────────────────────────────
|
|
35
15
|
/**
|
|
36
16
|
* Comma-separated chain of providers tried in order on retryable failures.
|
|
37
17
|
* Default: "openrouter,anthropic,openai" — OpenRouter first (multi-model),
|
|
38
18
|
* then direct Anthropic (prompt caching), then direct OpenAI as last resort.
|
|
39
19
|
*/
|
|
40
|
-
LLM_FALLBACK_CHAIN: FallbackChain.default(()
|
|
20
|
+
LLM_FALLBACK_CHAIN: FallbackChain.default(() => [
|
|
41
21
|
"openrouter",
|
|
42
22
|
"anthropic",
|
|
43
|
-
"openai"
|
|
23
|
+
"openai"
|
|
44
24
|
]),
|
|
45
|
-
|
|
46
25
|
/**
|
|
47
26
|
* Comma-separated chain of providers tried for EMBEDDINGS, in order.
|
|
48
27
|
* Default: "openrouter,openai" — Anthropic does not currently expose
|
|
49
28
|
* embedding models, so it is excluded by default. If unset, falls back
|
|
50
29
|
* to LLM_FALLBACK_CHAIN with embedding-incompatible providers filtered out.
|
|
51
30
|
*/
|
|
52
|
-
LLM_EMBEDDING_FALLBACK_CHAIN: FallbackChain.default(()
|
|
31
|
+
LLM_EMBEDDING_FALLBACK_CHAIN: FallbackChain.default(() => [
|
|
53
32
|
"openrouter",
|
|
54
|
-
"openai"
|
|
55
|
-
])
|
|
33
|
+
"openai"
|
|
34
|
+
])
|
|
56
35
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** Lazy parsed env — re-read once on first call. */
|
|
61
|
-
let _cached: AgentsEnv | undefined;
|
|
62
|
-
|
|
63
|
-
/** Returns the validated env (cached). Safe to call from any runtime. */
|
|
64
|
-
export function getAgentsEnv(): AgentsEnv {
|
|
36
|
+
var _cached;
|
|
37
|
+
function getAgentsEnv() {
|
|
65
38
|
if (_cached) return _cached;
|
|
66
39
|
_cached = AgentsEnvSchema.parse(globalThis.process?.env ?? {});
|
|
67
40
|
return _cached;
|
|
68
41
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export function _resetAgentsEnvCache(): void {
|
|
72
|
-
_cached = undefined;
|
|
42
|
+
function _resetAgentsEnvCache() {
|
|
43
|
+
_cached = void 0;
|
|
73
44
|
}
|
|
74
|
-
|
|
75
|
-
/** True iff Langfuse credentials are present. */
|
|
76
|
-
export function isLangfuseConfigured(): boolean {
|
|
45
|
+
function isLangfuseConfigured() {
|
|
77
46
|
const env = getAgentsEnv();
|
|
78
47
|
return Boolean(env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_SECRET_KEY);
|
|
79
48
|
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
AgentsEnvSchema,
|
|
52
|
+
getAgentsEnv,
|
|
53
|
+
_resetAgentsEnvCache,
|
|
54
|
+
isLangfuseConfigured
|
|
55
|
+
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// src/sdk/models.ts
|
|
2
2
|
var models = {
|
|
3
3
|
/** High-quality reasoning — default for complex tasks */
|
|
4
|
-
flagship: "anthropic/claude-sonnet-4",
|
|
4
|
+
flagship: "anthropic/claude-sonnet-4.6",
|
|
5
5
|
/** Deep reasoning for architecture and research */
|
|
6
|
-
reasoning: "anthropic/claude-opus-4",
|
|
6
|
+
reasoning: "anthropic/claude-opus-4.8",
|
|
7
7
|
/** Fast + cheap — chat, summaries, classification */
|
|
8
|
-
fast: "anthropic/claude-haiku-4",
|
|
8
|
+
fast: "anthropic/claude-haiku-4.5",
|
|
9
9
|
/** OpenAI flagship */
|
|
10
|
-
"openai-flagship": "openai/gpt-5.
|
|
10
|
+
"openai-flagship": "openai/gpt-5.5",
|
|
11
11
|
/** Google flagship */
|
|
12
|
-
"google-flagship": "google/gemini-
|
|
12
|
+
"google-flagship": "google/gemini-3.1-pro-preview",
|
|
13
13
|
/** Google fast */
|
|
14
|
-
"google-fast": "google/gemini-
|
|
14
|
+
"google-fast": "google/gemini-3.5-flash",
|
|
15
15
|
/** Embedding model */
|
|
16
16
|
embedding: "openai/text-embedding-3-small",
|
|
17
17
|
/** Embedding model (high-dimensional) */
|
|
@@ -22,7 +22,16 @@ var models = {
|
|
|
22
22
|
/** SiliconFlow — DeepSeek R1 reasoning (via SiliconFlow Pro) */
|
|
23
23
|
"sf-deepseek-r1": "Pro/deepseek-ai/DeepSeek-R1",
|
|
24
24
|
/** SiliconFlow — DeepSeek V3 (fast, capable) */
|
|
25
|
-
"sf-deepseek-v3": "deepseek-ai/DeepSeek-V3"
|
|
25
|
+
"sf-deepseek-v3": "deepseek-ai/DeepSeek-V3",
|
|
26
|
+
// --- SenseNova Token Plan presets (use with provider: "sensenova") ---
|
|
27
|
+
// Base URL: https://token.sensenova.cn/v1
|
|
28
|
+
// Docs: https://github.com/OpenSenseNova/SenseNova6.7/blob/main/API_CN.md
|
|
29
|
+
/** SenseNova 6.7 Flash-Lite — small multimodal agent (default for i18n CI) */
|
|
30
|
+
"sn-flash-lite": "sensenova-6.7-flash-lite",
|
|
31
|
+
/** DeepSeek V4 Flash via SenseNova Token Plan — long context, cheap */
|
|
32
|
+
"sn-deepseek-flash": "deepseek-v4-flash",
|
|
33
|
+
/** Alias: prefer flash-lite for bulk translation */
|
|
34
|
+
"sn-translate": "sensenova-6.7-flash-lite"
|
|
26
35
|
};
|
|
27
36
|
function resolveModel(modelOrPreset) {
|
|
28
37
|
if (modelOrPreset in models) {
|
|
@@ -1,20 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
runEmbedWithFallback
|
|
3
|
-
} from "./chunk-5LX742GP.js";
|
|
4
1
|
import {
|
|
5
2
|
createModel
|
|
6
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RNFUEMUB.js";
|
|
7
4
|
import {
|
|
8
5
|
NebutraAIConfigSchema
|
|
9
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-V6VC2O6Q.js";
|
|
7
|
+
import {
|
|
8
|
+
assertSafeOpenAIJsonPayload
|
|
9
|
+
} from "./chunk-VZPQOXWW.js";
|
|
10
|
+
import {
|
|
11
|
+
runEmbedWithFallback,
|
|
12
|
+
runWithFallback
|
|
13
|
+
} from "./chunk-XLBS3XUI.js";
|
|
10
14
|
|
|
11
15
|
// src/sdk/index.ts
|
|
12
16
|
import {
|
|
13
17
|
embed as _embed,
|
|
14
18
|
embedMany as _embedMany,
|
|
15
|
-
generateText as
|
|
19
|
+
generateText as _generateText2,
|
|
16
20
|
streamText as _streamText
|
|
17
21
|
} from "ai";
|
|
22
|
+
|
|
23
|
+
// src/sdk/structured.ts
|
|
24
|
+
import { generateText as _generateText, jsonSchema, tool } from "ai";
|
|
25
|
+
import Ajv from "ajv";
|
|
26
|
+
var ajv = new Ajv({ allErrors: true, strict: false });
|
|
27
|
+
function validateStructured(value, schema) {
|
|
28
|
+
const validate = ajv.compile(schema);
|
|
29
|
+
if (!validate(value)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`structured output failed schema validation: ${ajv.errorsText(validate.errors)}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function generateStructured(messages, schema, options = {}) {
|
|
36
|
+
assertSafeOpenAIJsonPayload("OpenAI structured request messages", messages);
|
|
37
|
+
assertSafeOpenAIJsonPayload("OpenAI structured output schema", schema);
|
|
38
|
+
const { result } = await runWithFallback(
|
|
39
|
+
(model) => _generateText({
|
|
40
|
+
model,
|
|
41
|
+
messages,
|
|
42
|
+
tools: {
|
|
43
|
+
_output: tool({
|
|
44
|
+
description: "Return the final result as a structured object matching the schema.",
|
|
45
|
+
inputSchema: jsonSchema(schema)
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
toolChoice: { type: "tool", toolName: "_output" }
|
|
49
|
+
}),
|
|
50
|
+
{ model: options.model ?? "flagship" }
|
|
51
|
+
);
|
|
52
|
+
const call = result.toolCalls?.find((c) => c.toolName === "_output");
|
|
53
|
+
const output = call?.input ?? null;
|
|
54
|
+
validateStructured(output, schema);
|
|
55
|
+
const usage = result.usage;
|
|
56
|
+
return {
|
|
57
|
+
output,
|
|
58
|
+
usage: {
|
|
59
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
60
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
61
|
+
reasoningOutputTokens: usage?.reasoningTokens ?? 0
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/sdk/index.ts
|
|
18
67
|
var _resolved = NebutraAIConfigSchema.parse({});
|
|
19
68
|
function configure(config = {}) {
|
|
20
69
|
_resolved = NebutraAIConfigSchema.parse(config);
|
|
@@ -23,8 +72,9 @@ function getConfig() {
|
|
|
23
72
|
return _resolved;
|
|
24
73
|
}
|
|
25
74
|
async function generateText(messages, options = {}) {
|
|
75
|
+
assertSafeOpenAIJsonPayload("OpenAI request messages", messages);
|
|
26
76
|
const model = createModel(options.model ?? _resolved.defaultModel, _resolved);
|
|
27
|
-
return await
|
|
77
|
+
return await _generateText2({
|
|
28
78
|
model,
|
|
29
79
|
messages,
|
|
30
80
|
...options.system ? { system: options.system } : {},
|
|
@@ -34,6 +84,7 @@ async function generateText(messages, options = {}) {
|
|
|
34
84
|
});
|
|
35
85
|
}
|
|
36
86
|
async function streamText(messages, options = {}) {
|
|
87
|
+
assertSafeOpenAIJsonPayload("OpenAI request messages", messages);
|
|
37
88
|
const model = createModel(options.model ?? _resolved.defaultModel, _resolved);
|
|
38
89
|
const userOnFinish = options.onFinish;
|
|
39
90
|
return _streamText({
|
|
@@ -73,6 +124,8 @@ async function embedMany(values, options = {}) {
|
|
|
73
124
|
}
|
|
74
125
|
|
|
75
126
|
export {
|
|
127
|
+
validateStructured,
|
|
128
|
+
generateStructured,
|
|
76
129
|
configure,
|
|
77
130
|
getConfig,
|
|
78
131
|
generateText,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAgentsEnv,
|
|
3
3
|
isLangfuseConfigured
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BMSL4E4A.js";
|
|
5
5
|
|
|
6
6
|
// src/observability.ts
|
|
7
7
|
import { logger } from "@nebutra/logger";
|
|
@@ -28,6 +28,9 @@ async function initLangfuse() {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function _resetLangfuseCache() {
|
|
32
|
+
_client = void 0;
|
|
33
|
+
}
|
|
31
34
|
function buildTelemetryConfig(args) {
|
|
32
35
|
if (!isLangfuseConfigured()) {
|
|
33
36
|
return { isEnabled: false };
|
|
@@ -55,6 +58,7 @@ async function flushTelemetry() {
|
|
|
55
58
|
|
|
56
59
|
export {
|
|
57
60
|
initLangfuse,
|
|
61
|
+
_resetLangfuseCache,
|
|
58
62
|
buildTelemetryConfig,
|
|
59
63
|
flushTelemetry
|
|
60
64
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveApiKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-V6VC2O6Q.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveModel
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BZKIMAGK.js";
|
|
7
7
|
|
|
8
8
|
// src/sdk/provider.ts
|
|
9
9
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
@@ -27,7 +27,14 @@ function createModel(modelOrPreset, config) {
|
|
|
27
27
|
case "siliconflow": {
|
|
28
28
|
const provider = createOpenAI({
|
|
29
29
|
apiKey,
|
|
30
|
-
baseURL: "https://api.siliconflow.cn/v1"
|
|
30
|
+
baseURL: process.env.SILICONFLOW_BASE_URL ?? "https://api.siliconflow.cn/v1"
|
|
31
|
+
});
|
|
32
|
+
return provider(modelId);
|
|
33
|
+
}
|
|
34
|
+
case "sensenova": {
|
|
35
|
+
const provider = createOpenAI({
|
|
36
|
+
apiKey,
|
|
37
|
+
baseURL: process.env.SENSENOVA_BASE_URL ?? "https://token.sensenova.cn/v1"
|
|
31
38
|
});
|
|
32
39
|
return provider(modelId);
|
|
33
40
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRetryableError
|
|
3
|
+
} from "./chunk-XLBS3XUI.js";
|
|
4
|
+
|
|
5
|
+
// src/generation/index.ts
|
|
6
|
+
import { logger } from "@nebutra/logger";
|
|
7
|
+
|
|
8
|
+
// src/generation/mock-provider.ts
|
|
9
|
+
function hash(input) {
|
|
10
|
+
let h = 2166136261;
|
|
11
|
+
for (let i = 0; i < input.length; i++) {
|
|
12
|
+
h ^= input.charCodeAt(i);
|
|
13
|
+
h = Math.imul(h, 16777619);
|
|
14
|
+
}
|
|
15
|
+
return h >>> 0;
|
|
16
|
+
}
|
|
17
|
+
function escapeXml(s) {
|
|
18
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19
|
+
}
|
|
20
|
+
function svgDataUri(label, prompt, w, h) {
|
|
21
|
+
const hue = hash(prompt) % 360;
|
|
22
|
+
const hue2 = (hue + 40) % 360;
|
|
23
|
+
const words = prompt.split(/\s+/);
|
|
24
|
+
const lines = [];
|
|
25
|
+
let cur = "";
|
|
26
|
+
for (const word of words) {
|
|
27
|
+
if ((cur + " " + word).trim().length > 32) {
|
|
28
|
+
lines.push(cur.trim());
|
|
29
|
+
cur = word;
|
|
30
|
+
} else {
|
|
31
|
+
cur = `${cur} ${word}`;
|
|
32
|
+
}
|
|
33
|
+
if (lines.length === 4) break;
|
|
34
|
+
}
|
|
35
|
+
if (cur && lines.length < 4) lines.push(cur.trim());
|
|
36
|
+
const tspans = lines.map((ln, i) => `<tspan x="50%" dy="${i === 0 ? 0 : 26}">${escapeXml(ln)}</tspan>`).join("");
|
|
37
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
38
|
+
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
39
|
+
<stop offset="0" stop-color="hsl(${hue} 70% 55%)"/>
|
|
40
|
+
<stop offset="1" stop-color="hsl(${hue2} 70% 45%)"/>
|
|
41
|
+
</linearGradient></defs>
|
|
42
|
+
<rect width="${w}" height="${h}" fill="url(#g)"/>
|
|
43
|
+
<text x="50%" y="14%" fill="rgba(255,255,255,.7)" font-family="sans-serif" font-size="20" text-anchor="middle">${escapeXml(label)}</text>
|
|
44
|
+
<text x="50%" y="46%" fill="#fff" font-family="sans-serif" font-size="22" font-weight="600" text-anchor="middle">${tspans}</text>
|
|
45
|
+
</svg>`;
|
|
46
|
+
const b64 = typeof btoa === "function" ? btoa(unescape(encodeURIComponent(svg))) : Buffer.from(svg, "utf8").toString("base64");
|
|
47
|
+
return `data:image/svg+xml;base64,${b64}`;
|
|
48
|
+
}
|
|
49
|
+
var mockGenerationProvider = {
|
|
50
|
+
name: "mock",
|
|
51
|
+
envKey: null,
|
|
52
|
+
capabilities: ["image", "video"],
|
|
53
|
+
async generateImage(req, _ctx) {
|
|
54
|
+
const width = req.width ?? 1024;
|
|
55
|
+
const height = req.height ?? 1024;
|
|
56
|
+
return {
|
|
57
|
+
modality: "image",
|
|
58
|
+
mimeType: "image/svg+xml",
|
|
59
|
+
url: svgDataUri("mock \xB7 image", req.prompt, width, height),
|
|
60
|
+
width,
|
|
61
|
+
height,
|
|
62
|
+
providerName: "mock",
|
|
63
|
+
model: req.model ?? "mock-image-1",
|
|
64
|
+
usage: { units: 1 }
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
async generateVideo(req, _ctx) {
|
|
68
|
+
const width = req.width ?? 1280;
|
|
69
|
+
const height = req.height ?? 720;
|
|
70
|
+
const seconds = req.durationSeconds ?? 5;
|
|
71
|
+
return {
|
|
72
|
+
modality: "video",
|
|
73
|
+
mimeType: "image/svg+xml",
|
|
74
|
+
url: svgDataUri(`mock \xB7 video \xB7 ${seconds}s`, req.prompt, width, height),
|
|
75
|
+
width,
|
|
76
|
+
height,
|
|
77
|
+
providerName: "mock",
|
|
78
|
+
model: req.model ?? "mock-video-1",
|
|
79
|
+
usage: { units: seconds }
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/generation/index.ts
|
|
85
|
+
var log = logger.child({ module: "agents/generation" });
|
|
86
|
+
var _registry = /* @__PURE__ */ new Map();
|
|
87
|
+
function registerGenerationProvider(provider) {
|
|
88
|
+
_registry.set(provider.name, provider);
|
|
89
|
+
}
|
|
90
|
+
function _resetGenerationRegistry() {
|
|
91
|
+
_registry.clear();
|
|
92
|
+
_registry.set(mockGenerationProvider.name, mockGenerationProvider);
|
|
93
|
+
}
|
|
94
|
+
_resetGenerationRegistry();
|
|
95
|
+
function hasEnvKey(provider) {
|
|
96
|
+
if (provider.envKey === null) return true;
|
|
97
|
+
return Boolean(globalThis.process?.env?.[provider.envKey]);
|
|
98
|
+
}
|
|
99
|
+
function listGenerationProviders(modality, options = {}) {
|
|
100
|
+
const envChain = (globalThis.process?.env?.GENERATION_FALLBACK_CHAIN ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
101
|
+
const preferred = options.chain ?? (envChain.length > 0 ? envChain : []);
|
|
102
|
+
const mockName = mockGenerationProvider.name;
|
|
103
|
+
const all = [...preferred, ..._registry.keys()];
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
const resolved = [];
|
|
106
|
+
for (const name of all) {
|
|
107
|
+
if (seen.has(name) || name === mockName) continue;
|
|
108
|
+
seen.add(name);
|
|
109
|
+
const provider = _registry.get(name);
|
|
110
|
+
if (!provider) continue;
|
|
111
|
+
if (!provider.capabilities.includes(modality)) continue;
|
|
112
|
+
if (!hasEnvKey(provider)) continue;
|
|
113
|
+
resolved.push(name);
|
|
114
|
+
}
|
|
115
|
+
resolved.push(mockName);
|
|
116
|
+
return resolved;
|
|
117
|
+
}
|
|
118
|
+
async function runChain(modality, options, ctx, invoke) {
|
|
119
|
+
const chain = listGenerationProviders(modality, options);
|
|
120
|
+
let lastErr;
|
|
121
|
+
for (const name of chain) {
|
|
122
|
+
const provider = _registry.get(name);
|
|
123
|
+
if (!provider) continue;
|
|
124
|
+
try {
|
|
125
|
+
const result = await invoke(provider);
|
|
126
|
+
log.debug("generation succeeded", {
|
|
127
|
+
provider: name,
|
|
128
|
+
modality,
|
|
129
|
+
tenantId: ctx.tenantId
|
|
130
|
+
});
|
|
131
|
+
return result;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
lastErr = err;
|
|
134
|
+
const retryable = isRetryableError(err);
|
|
135
|
+
log.warn("generation provider failed", {
|
|
136
|
+
provider: name,
|
|
137
|
+
modality,
|
|
138
|
+
retryable,
|
|
139
|
+
tenantId: ctx.tenantId
|
|
140
|
+
});
|
|
141
|
+
if (!retryable && name !== mockGenerationProvider.name) continue;
|
|
142
|
+
if (!retryable) throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw lastErr instanceof Error ? lastErr : new Error("[@nebutra/agents] generation chain exhausted");
|
|
146
|
+
}
|
|
147
|
+
async function generateImage(req, ctx, options = {}) {
|
|
148
|
+
return runChain("image", options, ctx, (p) => {
|
|
149
|
+
if (!p.generateImage) {
|
|
150
|
+
throw new Error(`[@nebutra/agents] provider "${p.name}" lacks image support`);
|
|
151
|
+
}
|
|
152
|
+
return p.generateImage(req, ctx);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async function generateVideo(req, ctx, options = {}) {
|
|
156
|
+
return runChain("video", options, ctx, (p) => {
|
|
157
|
+
if (!p.generateVideo) {
|
|
158
|
+
throw new Error(`[@nebutra/agents] provider "${p.name}" lacks video support`);
|
|
159
|
+
}
|
|
160
|
+
return p.generateVideo(req, ctx);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
mockGenerationProvider,
|
|
166
|
+
registerGenerationProvider,
|
|
167
|
+
_resetGenerationRegistry,
|
|
168
|
+
listGenerationProviders,
|
|
169
|
+
generateImage,
|
|
170
|
+
generateVideo
|
|
171
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// src/sdk/config.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var ProviderType = z.enum(["openrouter", "openai", "siliconflow", "gateway"]);
|
|
3
|
+
var ProviderType = z.enum(["openrouter", "openai", "siliconflow", "sensenova", "gateway"]);
|
|
4
4
|
var NebutraAIConfigSchema = z.object({
|
|
5
5
|
/** Which provider backend to use. Defaults to "openrouter". */
|
|
6
6
|
provider: ProviderType.default("openrouter"),
|
|
7
7
|
/** API key override. Falls back to env vars per provider. */
|
|
8
8
|
apiKey: z.string().optional(),
|
|
9
|
-
/** Default model
|
|
10
|
-
defaultModel: z.string().default("anthropic/claude-sonnet-4"),
|
|
9
|
+
/** Default model id — OpenRouter / models.dev frontier (not Claude 3.x / GPT-4 era). */
|
|
10
|
+
defaultModel: z.string().default("anthropic/claude-sonnet-4.6"),
|
|
11
11
|
/** Default temperature for generations. */
|
|
12
12
|
temperature: z.number().min(0).max(2).default(0.7),
|
|
13
13
|
/** Default max tokens for output. */
|
|
@@ -23,6 +23,7 @@ function resolveApiKey(config) {
|
|
|
23
23
|
openrouter: "OPENROUTER_API_KEY",
|
|
24
24
|
openai: "OPENAI_API_KEY",
|
|
25
25
|
siliconflow: "SILICONFLOW_API_KEY",
|
|
26
|
+
sensenova: "SENSENOVA_API_KEY",
|
|
26
27
|
gateway: "VERCEL_OIDC_TOKEN"
|
|
27
28
|
};
|
|
28
29
|
const envVar = envMap[config.provider];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/sdk/payload-guard.ts
|
|
2
|
+
var OPENAI_MAX_PROPERTY_NAME_LENGTH = 256;
|
|
3
|
+
function isObjectLike(value) {
|
|
4
|
+
return typeof value === "object" && value !== null;
|
|
5
|
+
}
|
|
6
|
+
function pathSegment(key) {
|
|
7
|
+
const preview = key.length > 32 ? `${key.slice(0, 29)}...` : key;
|
|
8
|
+
return /^[A-Za-z_$][\w$]*$/.test(preview) ? `.${preview}` : `[${JSON.stringify(preview)}]`;
|
|
9
|
+
}
|
|
10
|
+
function findOversizedPropertyName(value, maxLength = OPENAI_MAX_PROPERTY_NAME_LENGTH) {
|
|
11
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
12
|
+
function visit(candidate, path) {
|
|
13
|
+
if (!isObjectLike(candidate)) return null;
|
|
14
|
+
if (seen.has(candidate)) return null;
|
|
15
|
+
seen.add(candidate);
|
|
16
|
+
if (Array.isArray(candidate)) {
|
|
17
|
+
for (let index = 0; index < candidate.length; index += 1) {
|
|
18
|
+
const issue = visit(candidate[index], `${path}[${index}]`);
|
|
19
|
+
if (issue) return issue;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
for (const [key, nested] of Object.entries(candidate)) {
|
|
24
|
+
const nestedPath = `${path}${pathSegment(key)}`;
|
|
25
|
+
if (key.length > maxLength) {
|
|
26
|
+
return { path: nestedPath, length: key.length };
|
|
27
|
+
}
|
|
28
|
+
const issue = visit(nested, nestedPath);
|
|
29
|
+
if (issue) return issue;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return visit(value, "$");
|
|
34
|
+
}
|
|
35
|
+
function assertSafeOpenAIJsonPayload(label, value) {
|
|
36
|
+
const issue = findOversizedPropertyName(value);
|
|
37
|
+
if (!issue) return;
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${label} contains an object property name at ${issue.path} that is ${issue.length} characters long; maximum is ${OPENAI_MAX_PROPERTY_NAME_LENGTH}. Put generated text in a string value instead of using it as a JSON key.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
OPENAI_MAX_PROPERTY_NAME_LENGTH,
|
|
45
|
+
findOversizedPropertyName,
|
|
46
|
+
assertSafeOpenAIJsonPayload
|
|
47
|
+
};
|
|
@@ -1,52 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveModel
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var FallbackProviderName = z.enum(["openrouter", "anthropic", "openai"]);
|
|
8
|
-
var FallbackChain = z.string().transform(
|
|
9
|
-
(raw) => raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
10
|
-
).pipe(z.array(FallbackProviderName).min(1));
|
|
11
|
-
var AgentsEnvSchema = z.object({
|
|
12
|
-
// ── Anthropic (direct) ─────────────────────────────────────────────────
|
|
13
|
-
ANTHROPIC_API_KEY: z.string().optional(),
|
|
14
|
-
// ── Langfuse (LLM tracing — optional) ─────────────────────────────────
|
|
15
|
-
LANGFUSE_PUBLIC_KEY: z.string().optional(),
|
|
16
|
-
LANGFUSE_SECRET_KEY: z.string().optional(),
|
|
17
|
-
LANGFUSE_HOST: z.string().url().default("https://cloud.langfuse.com"),
|
|
18
|
-
// ── Multi-provider fallback chain ─────────────────────────────────────
|
|
19
|
-
/**
|
|
20
|
-
* Comma-separated chain of providers tried in order on retryable failures.
|
|
21
|
-
* Default: "openrouter,anthropic,openai" — OpenRouter first (multi-model),
|
|
22
|
-
* then direct Anthropic (prompt caching), then direct OpenAI as last resort.
|
|
23
|
-
*/
|
|
24
|
-
LLM_FALLBACK_CHAIN: FallbackChain.default(() => [
|
|
25
|
-
"openrouter",
|
|
26
|
-
"anthropic",
|
|
27
|
-
"openai"
|
|
28
|
-
]),
|
|
29
|
-
/**
|
|
30
|
-
* Comma-separated chain of providers tried for EMBEDDINGS, in order.
|
|
31
|
-
* Default: "openrouter,openai" — Anthropic does not currently expose
|
|
32
|
-
* embedding models, so it is excluded by default. If unset, falls back
|
|
33
|
-
* to LLM_FALLBACK_CHAIN with embedding-incompatible providers filtered out.
|
|
34
|
-
*/
|
|
35
|
-
LLM_EMBEDDING_FALLBACK_CHAIN: FallbackChain.default(() => [
|
|
36
|
-
"openrouter",
|
|
37
|
-
"openai"
|
|
38
|
-
])
|
|
39
|
-
});
|
|
40
|
-
var _cached;
|
|
41
|
-
function getAgentsEnv() {
|
|
42
|
-
if (_cached) return _cached;
|
|
43
|
-
_cached = AgentsEnvSchema.parse(globalThis.process?.env ?? {});
|
|
44
|
-
return _cached;
|
|
45
|
-
}
|
|
46
|
-
function isLangfuseConfigured() {
|
|
47
|
-
const env = getAgentsEnv();
|
|
48
|
-
return Boolean(env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_SECRET_KEY);
|
|
49
|
-
}
|
|
3
|
+
} from "./chunk-BZKIMAGK.js";
|
|
4
|
+
import {
|
|
5
|
+
getAgentsEnv
|
|
6
|
+
} from "./chunk-BMSL4E4A.js";
|
|
50
7
|
|
|
51
8
|
// src/fallback.ts
|
|
52
9
|
import { logger } from "@nebutra/logger";
|
|
@@ -212,9 +169,6 @@ async function runEmbedWithFallback(invoke, options = {}) {
|
|
|
212
169
|
}
|
|
213
170
|
|
|
214
171
|
export {
|
|
215
|
-
AgentsEnvSchema,
|
|
216
|
-
getAgentsEnv,
|
|
217
|
-
isLangfuseConfigured,
|
|
218
172
|
filterAvailableProviders,
|
|
219
173
|
isRetryableError,
|
|
220
174
|
runWithFallback,
|