@nebutra/agents 1.0.0 → 1.1.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/.turbo/turbo-build.log +40 -0
- package/.turbo/turbo-test.log +18 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +26 -0
- package/dist/agent-CahnMASx.d.ts +32 -0
- package/dist/chunk-5JZJ5KMC.js +37 -0
- package/dist/chunk-5LX742GP.js +224 -0
- package/dist/chunk-B7XWL35G.js +60 -0
- package/dist/chunk-NPQECBXL.js +82 -0
- package/dist/chunk-NVPE5EDI.js +42 -0
- package/dist/chunk-RDOFKRI6.js +166 -0
- package/dist/chunk-RLWM437Q.js +61 -0
- package/dist/chunk-UVL2UVVM.js +56 -0
- package/dist/index.d.ts +476 -0
- package/dist/index.js +533 -0
- package/dist/providers/langchain.d.ts +18 -0
- package/dist/providers/langchain.js +23 -0
- package/dist/providers/vercel-ai.d.ts +16 -0
- package/dist/providers/vercel-ai.js +83 -0
- package/dist/sdk/config.d.ts +48 -0
- package/dist/sdk/config.js +10 -0
- package/dist/sdk/index.d.ts +94 -0
- package/dist/sdk/index.js +33 -0
- package/dist/sdk/models.d.ts +40 -0
- package/dist/sdk/models.js +8 -0
- package/dist/sdk/provider.d.ts +21 -0
- package/dist/sdk/provider.js +10 -0
- package/dist/tools.d.ts +20 -0
- package/dist/tools.js +12 -0
- package/dist/types-NtgB3pch.d.ts +92 -0
- package/package.json +4 -3
- package/src/__tests__/generation.test.ts +111 -0
- package/src/generation/index.ts +157 -0
- package/src/generation/mock-provider.ts +123 -0
- package/src/generation/types.ts +87 -0
- package/src/index.ts +18 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runtime configuration for the Vercel AI SDK-backed helpers
|
|
5
|
+
* (`generateText`, `streamText`, `embed`, `embedMany`).
|
|
6
|
+
*
|
|
7
|
+
* Previously lived in `@nebutra/ai-sdk/config`. Absorbed into
|
|
8
|
+
* `@nebutra/agents` during the AI package consolidation so there is
|
|
9
|
+
* a single AI runtime package.
|
|
10
|
+
*/
|
|
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
|
+
declare const ProviderType: z.ZodEnum<{
|
|
21
|
+
openrouter: "openrouter";
|
|
22
|
+
openai: "openai";
|
|
23
|
+
siliconflow: "siliconflow";
|
|
24
|
+
gateway: "gateway";
|
|
25
|
+
}>;
|
|
26
|
+
type ProviderType = z.infer<typeof ProviderType>;
|
|
27
|
+
declare const NebutraAIConfigSchema: z.ZodObject<{
|
|
28
|
+
provider: z.ZodDefault<z.ZodEnum<{
|
|
29
|
+
openrouter: "openrouter";
|
|
30
|
+
openai: "openai";
|
|
31
|
+
siliconflow: "siliconflow";
|
|
32
|
+
gateway: "gateway";
|
|
33
|
+
}>>;
|
|
34
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
35
|
+
defaultModel: z.ZodDefault<z.ZodString>;
|
|
36
|
+
temperature: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
maxTokens: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
39
|
+
extraBody: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
type NebutraAIConfig = z.input<typeof NebutraAIConfigSchema>;
|
|
42
|
+
type ResolvedNebutraAIConfig = z.output<typeof NebutraAIConfigSchema>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the API key from explicit config or environment variables.
|
|
45
|
+
*/
|
|
46
|
+
declare function resolveApiKey(config: ResolvedNebutraAIConfig): string;
|
|
47
|
+
|
|
48
|
+
export { type NebutraAIConfig, NebutraAIConfigSchema, ProviderType, type ResolvedNebutraAIConfig, resolveApiKey };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as ai from 'ai';
|
|
2
|
+
import { JSONValue, ModelMessage, GenerateTextResult, StreamTextResult } from 'ai';
|
|
3
|
+
export { GenerateTextResult, ModelMessage, StreamTextResult } from 'ai';
|
|
4
|
+
import { NebutraAIConfig, ResolvedNebutraAIConfig } from './config.js';
|
|
5
|
+
export { NebutraAIConfigSchema, ProviderType } from './config.js';
|
|
6
|
+
export { ModelPreset, models, resolveModel } from './models.js';
|
|
7
|
+
export { createEmbeddingModel, createModel } from './provider.js';
|
|
8
|
+
import 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialise the global Nebutra AI configuration.
|
|
12
|
+
* Call once in your app entry point (e.g. instrumentation.ts or layout.tsx).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { configure } from "@nebutra/agents";
|
|
17
|
+
*
|
|
18
|
+
* configure({ provider: "openrouter" });
|
|
19
|
+
* // → reads OPENROUTER_API_KEY from env automatically
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare function configure(config?: NebutraAIConfig): void;
|
|
23
|
+
/** Returns the current resolved config (read-only). */
|
|
24
|
+
declare function getConfig(): Readonly<ResolvedNebutraAIConfig>;
|
|
25
|
+
/**
|
|
26
|
+
* Stream-completion event passed to {@link StreamOptions.onFinish}.
|
|
27
|
+
* Mirrors the AI SDK's `streamText.onFinish` event shape but is decoupled
|
|
28
|
+
* from the SDK internals so consumers don't break on SDK version bumps.
|
|
29
|
+
*/
|
|
30
|
+
interface StreamFinishEvent {
|
|
31
|
+
/** Final accumulated text of the model's response. */
|
|
32
|
+
text: string;
|
|
33
|
+
/** Reason the stream terminated (e.g. "stop", "length", "tool-calls"). */
|
|
34
|
+
finishReason: string;
|
|
35
|
+
/** Token usage for the request (best-effort; provider-dependent). */
|
|
36
|
+
usage: {
|
|
37
|
+
inputTokens: number | undefined;
|
|
38
|
+
outputTokens: number | undefined;
|
|
39
|
+
totalTokens: number | undefined;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
interface GenerateOptions {
|
|
43
|
+
/** Model ID or preset alias (e.g. "flagship", "fast", "anthropic/claude-sonnet-4"). */
|
|
44
|
+
model?: string;
|
|
45
|
+
/** System prompt prepended to the conversation. */
|
|
46
|
+
system?: string;
|
|
47
|
+
temperature?: number;
|
|
48
|
+
maxTokens?: number;
|
|
49
|
+
/** OpenRouter-specific provider options (reasoning, cacheControl, etc.). */
|
|
50
|
+
providerOptions?: Record<string, JSONValue | undefined>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Options accepted by {@link streamText}. Adds the durable `onFinish` hook on
|
|
54
|
+
* top of {@link GenerateOptions}.
|
|
55
|
+
*/
|
|
56
|
+
interface StreamOptions extends GenerateOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Invoked once the model has finished streaming, regardless of whether the
|
|
59
|
+
* client kept the response connection open. Use this for server-side
|
|
60
|
+
* persistence (e.g. saving chat sessions to a database) — it is the durable
|
|
61
|
+
* hook for write-once side-effects.
|
|
62
|
+
*
|
|
63
|
+
* Errors thrown inside the callback are caught by the AI SDK and logged;
|
|
64
|
+
* they do not surface to the streaming client.
|
|
65
|
+
*/
|
|
66
|
+
onFinish?: (event: StreamFinishEvent) => void | Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Generate a complete text response.
|
|
70
|
+
*/
|
|
71
|
+
declare function generateText(messages: ModelMessage[], options?: GenerateOptions): Promise<GenerateTextResult<Record<string, never>, never>>;
|
|
72
|
+
/**
|
|
73
|
+
* Stream a text response for real-time UI.
|
|
74
|
+
*/
|
|
75
|
+
declare function streamText(messages: ModelMessage[], options?: StreamOptions): Promise<StreamTextResult<Record<string, never>, never>>;
|
|
76
|
+
interface EmbedOptions {
|
|
77
|
+
/** Embedding model ID or preset alias. Defaults to "embedding". */
|
|
78
|
+
model?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Generate an embedding vector for a single value.
|
|
82
|
+
*
|
|
83
|
+
* Uses `runEmbedWithFallback()` so retryable failures (429 / 5xx / network)
|
|
84
|
+
* automatically rotate to the next provider in `LLM_EMBEDDING_FALLBACK_CHAIN`.
|
|
85
|
+
*/
|
|
86
|
+
declare function embed(value: string, options?: EmbedOptions): Promise<ai.EmbedResult>;
|
|
87
|
+
/**
|
|
88
|
+
* Generate embedding vectors for multiple values in a single request.
|
|
89
|
+
*
|
|
90
|
+
* Uses `runEmbedWithFallback()` for provider rotation on retryable errors.
|
|
91
|
+
*/
|
|
92
|
+
declare function embedMany(values: string[], options?: EmbedOptions): Promise<ai.EmbedManyResult>;
|
|
93
|
+
|
|
94
|
+
export { type EmbedOptions, type GenerateOptions, NebutraAIConfig, ResolvedNebutraAIConfig, type StreamFinishEvent, type StreamOptions, configure, embed, embedMany, generateText, getConfig, streamText };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configure,
|
|
3
|
+
embed,
|
|
4
|
+
embedMany,
|
|
5
|
+
generateText,
|
|
6
|
+
getConfig,
|
|
7
|
+
streamText
|
|
8
|
+
} from "../chunk-NPQECBXL.js";
|
|
9
|
+
import "../chunk-5LX742GP.js";
|
|
10
|
+
import {
|
|
11
|
+
createEmbeddingModel,
|
|
12
|
+
createModel
|
|
13
|
+
} from "../chunk-RLWM437Q.js";
|
|
14
|
+
import {
|
|
15
|
+
NebutraAIConfigSchema
|
|
16
|
+
} from "../chunk-NVPE5EDI.js";
|
|
17
|
+
import {
|
|
18
|
+
models,
|
|
19
|
+
resolveModel
|
|
20
|
+
} from "../chunk-5JZJ5KMC.js";
|
|
21
|
+
export {
|
|
22
|
+
NebutraAIConfigSchema,
|
|
23
|
+
configure,
|
|
24
|
+
createEmbeddingModel,
|
|
25
|
+
createModel,
|
|
26
|
+
embed,
|
|
27
|
+
embedMany,
|
|
28
|
+
generateText,
|
|
29
|
+
getConfig,
|
|
30
|
+
models,
|
|
31
|
+
resolveModel,
|
|
32
|
+
streamText
|
|
33
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
declare const models: {
|
|
10
|
+
/** High-quality reasoning — default for complex tasks */
|
|
11
|
+
readonly flagship: "anthropic/claude-sonnet-4";
|
|
12
|
+
/** Deep reasoning for architecture and research */
|
|
13
|
+
readonly reasoning: "anthropic/claude-opus-4";
|
|
14
|
+
/** Fast + cheap — chat, summaries, classification */
|
|
15
|
+
readonly fast: "anthropic/claude-haiku-4";
|
|
16
|
+
/** OpenAI flagship */
|
|
17
|
+
readonly "openai-flagship": "openai/gpt-5.4";
|
|
18
|
+
/** Google flagship */
|
|
19
|
+
readonly "google-flagship": "google/gemini-2.5-pro";
|
|
20
|
+
/** Google fast */
|
|
21
|
+
readonly "google-fast": "google/gemini-2.5-flash";
|
|
22
|
+
/** Embedding model */
|
|
23
|
+
readonly embedding: "openai/text-embedding-3-small";
|
|
24
|
+
/** Embedding model (high-dimensional) */
|
|
25
|
+
readonly "embedding-large": "openai/text-embedding-3-large";
|
|
26
|
+
/** SiliconFlow — Qwen 2.5 72B (flagship open-source) */
|
|
27
|
+
readonly "sf-qwen": "Qwen/Qwen2.5-72B-Instruct";
|
|
28
|
+
/** SiliconFlow — DeepSeek R1 reasoning (via SiliconFlow Pro) */
|
|
29
|
+
readonly "sf-deepseek-r1": "Pro/deepseek-ai/DeepSeek-R1";
|
|
30
|
+
/** SiliconFlow — DeepSeek V3 (fast, capable) */
|
|
31
|
+
readonly "sf-deepseek-v3": "deepseek-ai/DeepSeek-V3";
|
|
32
|
+
};
|
|
33
|
+
type ModelPreset = keyof typeof models;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves a model preset alias to its full model ID.
|
|
36
|
+
* If the input is not a preset key, returns it as-is (passthrough).
|
|
37
|
+
*/
|
|
38
|
+
declare function resolveModel(modelOrPreset: string): string;
|
|
39
|
+
|
|
40
|
+
export { type ModelPreset, models, resolveModel };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EmbeddingModel, LanguageModel } from 'ai';
|
|
2
|
+
import { ResolvedNebutraAIConfig } from './config.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a language model instance based on the resolved config.
|
|
7
|
+
*
|
|
8
|
+
* Provider routing:
|
|
9
|
+
* - "openrouter" → @openrouter/ai-sdk-provider (300+ models, failover)
|
|
10
|
+
* - "openai" → @ai-sdk/openai (direct OpenAI API)
|
|
11
|
+
* - "siliconflow" → @ai-sdk/openai with SiliconFlow baseURL (OpenAI-compatible)
|
|
12
|
+
* - "gateway" → Vercel AI Gateway via plain model string (OIDC auth)
|
|
13
|
+
*/
|
|
14
|
+
declare function createModel(modelOrPreset: string, config: ResolvedNebutraAIConfig): LanguageModel;
|
|
15
|
+
/**
|
|
16
|
+
* Creates an embedding model instance for vector operations.
|
|
17
|
+
* Only supported with OpenRouter provider.
|
|
18
|
+
*/
|
|
19
|
+
declare function createEmbeddingModel(modelOrPreset: string, config: ResolvedNebutraAIConfig): EmbeddingModel;
|
|
20
|
+
|
|
21
|
+
export { createEmbeddingModel, createModel };
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { d as AgentTool } from './types-NtgB3pch.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in tool registry for agents.
|
|
5
|
+
*
|
|
6
|
+
* These are safe, tenant-scoped stubs that demonstrate the tool interface.
|
|
7
|
+
* Each tool requires configuration (API keys, vector stores, etc.) before
|
|
8
|
+
* it produces real results.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Search the web for current information. */
|
|
12
|
+
declare const webSearchTool: AgentTool;
|
|
13
|
+
/** Query the tenant's database (tenant-scoped via RLS). */
|
|
14
|
+
declare const databaseQueryTool: AgentTool;
|
|
15
|
+
/** RAG-style knowledge base retrieval. */
|
|
16
|
+
declare const knowledgeBaseTool: AgentTool;
|
|
17
|
+
/** All pre-built tools. */
|
|
18
|
+
declare const BUILT_IN_TOOLS: readonly AgentTool[];
|
|
19
|
+
|
|
20
|
+
export { BUILT_IN_TOOLS, databaseQueryTool, knowledgeBaseTool, webSearchTool };
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the multi-agent orchestration engine.
|
|
3
|
+
*
|
|
4
|
+
* All tenant-scoped operations require an AgentContext carrying `tenantId`.
|
|
5
|
+
* Usage tracking is emitted on every execution for downstream billing.
|
|
6
|
+
*/
|
|
7
|
+
interface AgentConfig {
|
|
8
|
+
/** Unique identifier for this agent */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** Human-readable name */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** What this agent does (used by the router for intent matching) */
|
|
13
|
+
readonly description: string;
|
|
14
|
+
/** Model identifier, e.g. "openai/gpt-5.4" or "anthropic/claude-sonnet-4.6" */
|
|
15
|
+
readonly model: string;
|
|
16
|
+
/** System prompt / instructions */
|
|
17
|
+
readonly instructions: string;
|
|
18
|
+
/** Tools this agent can invoke */
|
|
19
|
+
readonly tools?: readonly AgentTool[];
|
|
20
|
+
/** Maximum tool-loop iterations (default 20) */
|
|
21
|
+
readonly maxSteps?: number;
|
|
22
|
+
/** Memory configuration */
|
|
23
|
+
readonly memory?: MemoryConfig;
|
|
24
|
+
}
|
|
25
|
+
interface AgentTool {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly description: string;
|
|
28
|
+
readonly inputSchema: Record<string, unknown>;
|
|
29
|
+
readonly execute: (input: unknown, context: AgentContext) => Promise<unknown>;
|
|
30
|
+
}
|
|
31
|
+
interface AgentContext {
|
|
32
|
+
readonly tenantId: string;
|
|
33
|
+
readonly userId: string;
|
|
34
|
+
readonly conversationId: string;
|
|
35
|
+
readonly metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
interface AgentMessage {
|
|
38
|
+
readonly role: "user" | "assistant" | "system" | "tool";
|
|
39
|
+
readonly content: string;
|
|
40
|
+
readonly toolCalls?: readonly ToolCallResult[];
|
|
41
|
+
readonly timestamp: Date;
|
|
42
|
+
}
|
|
43
|
+
interface ToolCallResult {
|
|
44
|
+
readonly toolName: string;
|
|
45
|
+
readonly args: unknown;
|
|
46
|
+
readonly result: unknown;
|
|
47
|
+
}
|
|
48
|
+
interface AgentResponse {
|
|
49
|
+
readonly messages: readonly AgentMessage[];
|
|
50
|
+
readonly usage: TokenUsage;
|
|
51
|
+
readonly finishReason: string;
|
|
52
|
+
readonly agentId: string;
|
|
53
|
+
}
|
|
54
|
+
interface TokenUsage {
|
|
55
|
+
readonly promptTokens: number;
|
|
56
|
+
readonly completionTokens: number;
|
|
57
|
+
readonly totalTokens: number;
|
|
58
|
+
}
|
|
59
|
+
interface MemoryConfig {
|
|
60
|
+
readonly shortTerm?: {
|
|
61
|
+
readonly maxMessages: number;
|
|
62
|
+
};
|
|
63
|
+
readonly longTerm?: {
|
|
64
|
+
readonly enabled: boolean;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
interface OrchestratorConfig {
|
|
68
|
+
readonly agents: readonly AgentConfig[];
|
|
69
|
+
readonly router?: RouterConfig;
|
|
70
|
+
readonly defaultAgentId?: string;
|
|
71
|
+
}
|
|
72
|
+
interface RouterConfig {
|
|
73
|
+
readonly strategy: "keyword" | "llm" | "custom";
|
|
74
|
+
readonly customRouter?: (message: string, context: AgentContext) => Promise<string>;
|
|
75
|
+
}
|
|
76
|
+
interface PipelineStep {
|
|
77
|
+
readonly agentId: string;
|
|
78
|
+
readonly transformInput?: (prevOutput: string) => string;
|
|
79
|
+
}
|
|
80
|
+
interface AgentUsageEvent {
|
|
81
|
+
readonly tenantId: string;
|
|
82
|
+
readonly userId: string;
|
|
83
|
+
readonly agentId: string;
|
|
84
|
+
readonly model: string;
|
|
85
|
+
readonly promptTokens: number;
|
|
86
|
+
readonly completionTokens: number;
|
|
87
|
+
readonly totalTokens: number;
|
|
88
|
+
readonly durationMs: number;
|
|
89
|
+
readonly timestamp: Date;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { AgentMessage as A, MemoryConfig as M, OrchestratorConfig as O, PipelineStep as P, RouterConfig as R, TokenUsage as T, AgentContext as a, AgentResponse as b, AgentConfig as c, AgentTool as d, AgentUsageEvent as e, ToolCallResult as f };
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Nebutra AI runtime: multi-agent orchestration + Vercel AI SDK helpers (absorbed @nebutra/ai-sdk in 1.0.0)",
|
|
5
5
|
"private": false,
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"nebutra": {
|
|
9
9
|
"featureId": "agents",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./tools": "./src/tools.ts",
|
|
18
18
|
"./providers/vercel-ai": "./src/providers/vercel-ai.ts",
|
|
19
19
|
"./providers/langchain": "./src/providers/langchain.ts",
|
|
20
|
+
"./generation": "./src/generation/index.ts",
|
|
20
21
|
"./sdk": "./src/sdk/index.ts",
|
|
21
22
|
"./sdk/config": "./src/sdk/config.ts",
|
|
22
23
|
"./sdk/models": "./src/sdk/models.ts",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"langfuse": "^3.38.20",
|
|
33
34
|
"langfuse-vercel": "^3.38.20",
|
|
34
35
|
"zod": "^4.3.6",
|
|
35
|
-
"@nebutra/billing": "0.1.
|
|
36
|
+
"@nebutra/billing": "0.1.1",
|
|
36
37
|
"@nebutra/cache": "0.0.1",
|
|
37
38
|
"@nebutra/logger": "0.1.0"
|
|
38
39
|
},
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image / video generation modality.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the env-key-gated provider chain mirrors the LLM fallback design:
|
|
5
|
+
* deterministic mock terminal, additive real-provider priority, retryable
|
|
6
|
+
* rotation, and tenant-scoped attribution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
_resetGenerationRegistry,
|
|
12
|
+
type GenerationContext,
|
|
13
|
+
type GenerationProvider,
|
|
14
|
+
generateImage,
|
|
15
|
+
generateVideo,
|
|
16
|
+
listGenerationProviders,
|
|
17
|
+
registerGenerationProvider,
|
|
18
|
+
} from "../generation/index";
|
|
19
|
+
|
|
20
|
+
const ctx: GenerationContext = {
|
|
21
|
+
tenantId: "org_test",
|
|
22
|
+
userId: "user_test",
|
|
23
|
+
conversationId: "canvas_1",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
_resetGenerationRegistry();
|
|
28
|
+
delete process.env.GENERATION_FALLBACK_CHAIN;
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
_resetGenerationRegistry();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("mock provider (terminal)", () => {
|
|
35
|
+
it("is always available with no secret", () => {
|
|
36
|
+
expect(listGenerationProviders("image")).toEqual(["mock"]);
|
|
37
|
+
expect(listGenerationProviders("video")).toEqual(["mock"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("produces a deterministic data URI for identical input", async () => {
|
|
41
|
+
const a = await generateImage({ prompt: "a blue fox", width: 512, height: 512 }, ctx);
|
|
42
|
+
const b = await generateImage({ prompt: "a blue fox", width: 512, height: 512 }, ctx);
|
|
43
|
+
expect(a.url).toBe(b.url);
|
|
44
|
+
expect(a.url.startsWith("data:image/svg+xml;base64,")).toBe(true);
|
|
45
|
+
expect(a.width).toBe(512);
|
|
46
|
+
expect(a.providerName).toBe("mock");
|
|
47
|
+
expect(a.usage.units).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("varies output by prompt", async () => {
|
|
51
|
+
const a = await generateImage({ prompt: "sunset" }, ctx);
|
|
52
|
+
const b = await generateImage({ prompt: "moonrise" }, ctx);
|
|
53
|
+
expect(a.url).not.toBe(b.url);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns a poster frame + second-based units for video", async () => {
|
|
57
|
+
const v = await generateVideo({ prompt: "waves", durationSeconds: 8 }, ctx);
|
|
58
|
+
expect(v.modality).toBe("video");
|
|
59
|
+
expect(v.usage.units).toBe(8);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("env-key gating + priority", () => {
|
|
64
|
+
const realProvider: GenerationProvider = {
|
|
65
|
+
name: "fake-real",
|
|
66
|
+
envKey: "FAKE_REAL_API_KEY",
|
|
67
|
+
capabilities: ["image"],
|
|
68
|
+
async generateImage(req) {
|
|
69
|
+
return {
|
|
70
|
+
modality: "image",
|
|
71
|
+
mimeType: "image/png",
|
|
72
|
+
url: "https://example.test/real.png",
|
|
73
|
+
width: req.width ?? 1024,
|
|
74
|
+
height: req.height ?? 1024,
|
|
75
|
+
providerName: "fake-real",
|
|
76
|
+
model: "fake-real-1",
|
|
77
|
+
usage: { units: 1 },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
it("hides a provider whose env key is absent", () => {
|
|
83
|
+
registerGenerationProvider(realProvider);
|
|
84
|
+
delete process.env.FAKE_REAL_API_KEY;
|
|
85
|
+
expect(listGenerationProviders("image")).toEqual(["mock"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("prefers the real provider when its key is present", async () => {
|
|
89
|
+
registerGenerationProvider(realProvider);
|
|
90
|
+
process.env.FAKE_REAL_API_KEY = "x";
|
|
91
|
+
expect(listGenerationProviders("image")).toEqual(["fake-real", "mock"]);
|
|
92
|
+
const r = await generateImage({ prompt: "hi" }, ctx);
|
|
93
|
+
expect(r.providerName).toBe("fake-real");
|
|
94
|
+
delete process.env.FAKE_REAL_API_KEY;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("retryable rotation", () => {
|
|
99
|
+
it("rotates to mock when a real provider throws a retryable error", async () => {
|
|
100
|
+
registerGenerationProvider({
|
|
101
|
+
name: "flaky",
|
|
102
|
+
envKey: null,
|
|
103
|
+
capabilities: ["image"],
|
|
104
|
+
async generateImage() {
|
|
105
|
+
throw new Error("429 Too Many Requests");
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const r = await generateImage({ prompt: "resilient" }, ctx);
|
|
109
|
+
expect(r.providerName).toBe("mock");
|
|
110
|
+
});
|
|
111
|
+
});
|