veryfront 0.1.13 → 0.1.14
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/esm/cli/app/data/slug-words.d.ts.map +1 -1
- package/esm/cli/app/data/slug-words.js +225 -90
- package/esm/cli/app/operations/project-creation.js +4 -3
- package/esm/cli/app/shell.js +1 -1
- package/esm/cli/app/utils.d.ts +5 -4
- package/esm/cli/app/utils.d.ts.map +1 -1
- package/esm/cli/app/utils.js +0 -23
- package/esm/cli/app/views/dashboard.d.ts +1 -1
- package/esm/cli/app/views/dashboard.d.ts.map +1 -1
- package/esm/cli/app/views/dashboard.js +22 -4
- package/esm/cli/auth/callback-server.d.ts.map +1 -1
- package/esm/cli/auth/callback-server.js +3 -2
- package/esm/cli/commands/dev/handler.d.ts.map +1 -1
- package/esm/cli/commands/dev/handler.js +2 -0
- package/esm/cli/commands/init/init-command.d.ts.map +1 -1
- package/esm/cli/commands/init/init-command.js +20 -3
- package/esm/cli/commands/init/interactive-wizard.d.ts +3 -2
- package/esm/cli/commands/init/interactive-wizard.d.ts.map +1 -1
- package/esm/cli/commands/init/interactive-wizard.js +55 -27
- package/esm/cli/mcp/remote-file-tools.d.ts +0 -6
- package/esm/cli/mcp/remote-file-tools.d.ts.map +1 -1
- package/esm/cli/mcp/remote-file-tools.js +37 -15
- package/esm/cli/shared/reserve-slug.d.ts.map +1 -1
- package/esm/cli/shared/reserve-slug.js +8 -3
- package/esm/cli/utils/env-prompt.d.ts.map +1 -1
- package/esm/cli/utils/env-prompt.js +3 -0
- package/esm/deno.d.ts +2 -1
- package/esm/deno.js +8 -4
- package/esm/src/agent/chat-handler.d.ts +4 -3
- package/esm/src/agent/chat-handler.d.ts.map +1 -1
- package/esm/src/agent/chat-handler.js +55 -4
- package/esm/src/agent/react/index.d.ts +1 -1
- package/esm/src/agent/react/index.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts +18 -0
- package/esm/src/agent/react/use-chat/browser-inference/browser-engine.d.ts.map +1 -0
- package/esm/src/agent/react/use-chat/browser-inference/browser-engine.js +54 -0
- package/esm/src/agent/react/use-chat/browser-inference/types.d.ts +43 -0
- package/esm/src/agent/react/use-chat/browser-inference/types.d.ts.map +1 -0
- package/esm/src/agent/react/use-chat/browser-inference/types.js +4 -0
- package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts +23 -0
- package/esm/src/agent/react/use-chat/browser-inference/worker-client.d.ts.map +1 -0
- package/esm/src/agent/react/use-chat/browser-inference/worker-client.js +67 -0
- package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts +8 -0
- package/esm/src/agent/react/use-chat/browser-inference/worker-script.d.ts.map +1 -0
- package/esm/src/agent/react/use-chat/browser-inference/worker-script.js +97 -0
- package/esm/src/agent/react/use-chat/index.d.ts +1 -1
- package/esm/src/agent/react/use-chat/index.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/types.d.ts +12 -0
- package/esm/src/agent/react/use-chat/types.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/use-chat.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/use-chat.js +120 -6
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +59 -7
- package/esm/src/build/production-build/templates.d.ts +2 -2
- package/esm/src/build/production-build/templates.d.ts.map +1 -1
- package/esm/src/build/production-build/templates.js +2 -68
- package/esm/src/chat/index.d.ts +1 -1
- package/esm/src/chat/index.d.ts.map +1 -1
- package/esm/src/errors/veryfront-error.d.ts +3 -0
- package/esm/src/errors/veryfront-error.d.ts.map +1 -1
- package/esm/src/platform/adapters/runtime/deno/adapter.d.ts.map +1 -1
- package/esm/src/platform/adapters/runtime/deno/adapter.js +5 -1
- package/esm/src/platform/compat/http/deno-server.d.ts.map +1 -1
- package/esm/src/platform/compat/http/deno-server.js +3 -2
- package/esm/src/provider/index.d.ts +1 -1
- package/esm/src/provider/index.d.ts.map +1 -1
- package/esm/src/provider/index.js +1 -1
- package/esm/src/provider/local/ai-sdk-adapter.d.ts +19 -0
- package/esm/src/provider/local/ai-sdk-adapter.d.ts.map +1 -0
- package/esm/src/provider/local/ai-sdk-adapter.js +164 -0
- package/esm/src/provider/local/env.d.ts +10 -0
- package/esm/src/provider/local/env.d.ts.map +1 -0
- package/esm/src/provider/local/env.js +23 -0
- package/esm/src/provider/local/local-engine.d.ts +61 -0
- package/esm/src/provider/local/local-engine.d.ts.map +1 -0
- package/esm/src/provider/local/local-engine.js +211 -0
- package/esm/src/provider/local/model-catalog.d.ts +30 -0
- package/esm/src/provider/local/model-catalog.d.ts.map +1 -0
- package/esm/src/provider/local/model-catalog.js +58 -0
- package/esm/src/provider/model-registry.d.ts +14 -0
- package/esm/src/provider/model-registry.d.ts.map +1 -1
- package/esm/src/provider/model-registry.js +58 -2
- package/esm/src/proxy/main.js +34 -6
- package/esm/src/proxy/server-resolver.d.ts +23 -0
- package/esm/src/proxy/server-resolver.d.ts.map +1 -0
- package/esm/src/proxy/server-resolver.js +124 -0
- package/esm/src/react/components/ai/chat/components/inference-badge.d.ts +8 -0
- package/esm/src/react/components/ai/chat/components/inference-badge.d.ts.map +1 -0
- package/esm/src/react/components/ai/chat/components/inference-badge.js +36 -0
- package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts +7 -0
- package/esm/src/react/components/ai/chat/components/upgrade-cta.d.ts.map +1 -0
- package/esm/src/react/components/ai/chat/components/upgrade-cta.js +33 -0
- package/esm/src/react/components/ai/chat/index.d.ts +7 -1
- package/esm/src/react/components/ai/chat/index.d.ts.map +1 -1
- package/esm/src/react/components/ai/chat/index.js +16 -4
- package/package.json +5 -1
- package/src/cli/app/data/slug-words.ts +225 -90
- package/src/cli/app/operations/project-creation.ts +3 -3
- package/src/cli/app/shell.ts +1 -1
- package/src/cli/app/utils.ts +0 -30
- package/src/cli/app/views/dashboard.ts +27 -4
- package/src/cli/auth/callback-server.ts +3 -2
- package/src/cli/commands/dev/handler.ts +2 -0
- package/src/cli/commands/init/init-command.ts +30 -3
- package/src/cli/commands/init/interactive-wizard.ts +62 -34
- package/src/cli/mcp/remote-file-tools.ts +50 -15
- package/src/cli/shared/reserve-slug.ts +9 -2
- package/src/cli/utils/env-prompt.ts +3 -0
- package/src/deno.js +8 -4
- package/src/src/agent/chat-handler.ts +57 -4
- package/src/src/agent/react/index.ts +2 -0
- package/src/src/agent/react/use-chat/browser-inference/browser-engine.ts +81 -0
- package/src/src/agent/react/use-chat/browser-inference/types.ts +52 -0
- package/src/src/agent/react/use-chat/browser-inference/worker-client.ts +89 -0
- package/src/src/agent/react/use-chat/browser-inference/worker-script.ts +98 -0
- package/src/src/agent/react/use-chat/index.ts +2 -0
- package/src/src/agent/react/use-chat/types.ts +20 -0
- package/src/src/agent/react/use-chat/use-chat.ts +148 -8
- package/src/src/agent/runtime/index.ts +72 -6
- package/src/src/build/production-build/templates.ts +2 -68
- package/src/src/chat/index.ts +2 -0
- package/src/src/errors/veryfront-error.ts +2 -1
- package/src/src/platform/adapters/runtime/deno/adapter.ts +5 -1
- package/src/src/platform/compat/http/deno-server.ts +3 -1
- package/src/src/provider/index.ts +1 -0
- package/src/src/provider/local/ai-sdk-adapter.ts +207 -0
- package/src/src/provider/local/env.ts +26 -0
- package/src/src/provider/local/local-engine.ts +288 -0
- package/src/src/provider/local/model-catalog.ts +73 -0
- package/src/src/provider/model-registry.ts +66 -2
- package/src/src/proxy/main.ts +41 -6
- package/src/src/proxy/server-resolver.ts +151 -0
- package/src/src/react/components/ai/chat/components/inference-badge.tsx +48 -0
- package/src/src/react/components/ai/chat/components/upgrade-cta.tsx +56 -0
- package/src/src/react/components/ai/chat/index.tsx +43 -6
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type MessagePart,
|
|
22
22
|
type ToolCall,
|
|
23
23
|
} from "../types.js";
|
|
24
|
-
import { resolveModel } from "../../provider/index.js";
|
|
24
|
+
import { ensureModelReady, resolveModel } from "../../provider/index.js";
|
|
25
25
|
import { executeTool } from "../../tool/index.js";
|
|
26
26
|
import { generateId } from "../../utils/id.js";
|
|
27
27
|
import { detectPlatform, getPlatformCapabilities } from "../../platform/core-platform.js";
|
|
@@ -36,7 +36,7 @@ import { convertToModelMessages } from "./model-message-converter.js";
|
|
|
36
36
|
import { convertToolsToAISDK } from "./model-tool-converter.js";
|
|
37
37
|
import { createStreamState, processStream } from "./ai-stream-handler.js";
|
|
38
38
|
import { MiddlewareChain } from "../middleware/chain.js";
|
|
39
|
-
import { generateText, streamText } from "ai";
|
|
39
|
+
import { generateText, type LanguageModel, streamText } from "ai";
|
|
40
40
|
|
|
41
41
|
// Re-export from submodules
|
|
42
42
|
export { generateMessageId, sendSSE } from "./sse-utils.js";
|
|
@@ -59,6 +59,27 @@ import { accumulateUsage, getMaxSteps, normalizeInput } from "./input-utils.js";
|
|
|
59
59
|
|
|
60
60
|
const logger = serverLogger.component("agent");
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Detect whether the resolved model is local inference.
|
|
64
|
+
* Handles both explicit "local/*" requests and cloud->local auto-fallback.
|
|
65
|
+
*/
|
|
66
|
+
function isLocalInferenceModel(model: LanguageModel, requestedModel: string): boolean {
|
|
67
|
+
if (requestedModel.startsWith("local/")) return true;
|
|
68
|
+
|
|
69
|
+
// LanguageModel is a union that includes string, so we need to narrow first
|
|
70
|
+
if (typeof model === "string") return model.startsWith("local/");
|
|
71
|
+
|
|
72
|
+
if ("provider" in model && model.provider === "local") return true;
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
"modelId" in model && typeof model.modelId === "string" && model.modelId.startsWith("local/")
|
|
76
|
+
) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
export class AgentRuntime {
|
|
63
84
|
private id: string;
|
|
64
85
|
private config: AgentConfig;
|
|
@@ -125,6 +146,7 @@ export class AgentRuntime {
|
|
|
125
146
|
modelOverride?: string,
|
|
126
147
|
): Promise<ReadableStream<Uint8Array>> {
|
|
127
148
|
const modelString = modelOverride || this.config.model;
|
|
149
|
+
const requestedModel = modelString || this.config.model;
|
|
128
150
|
|
|
129
151
|
for (const msg of messages) await this.memory.add(msg);
|
|
130
152
|
|
|
@@ -135,6 +157,18 @@ export class AgentRuntime {
|
|
|
135
157
|
const toolContext = { agentId: this.id, ...context };
|
|
136
158
|
const textPartId = generateId("text");
|
|
137
159
|
|
|
160
|
+
// Resolve model BEFORE creating the ReadableStream — if this throws
|
|
161
|
+
// (e.g., no_ai_available), the error propagates to the caller who can
|
|
162
|
+
// return a proper error response (503) instead of a 200 with an error event.
|
|
163
|
+
const languageModel = resolveModel(requestedModel);
|
|
164
|
+
|
|
165
|
+
// Eagerly verify the model runtime is available. For local models this
|
|
166
|
+
// checks that @huggingface/transformers can be imported. Must happen
|
|
167
|
+
// BEFORE creating the ReadableStream so no_ai_available errors propagate
|
|
168
|
+
// to the caller (createChatHandler) who returns a 503 with browser fallback
|
|
169
|
+
// info, instead of being swallowed as an in-band SSE error in a 200 response.
|
|
170
|
+
await ensureModelReady(languageModel);
|
|
171
|
+
|
|
138
172
|
return new ReadableStream<Uint8Array>({
|
|
139
173
|
start: async (controller) => {
|
|
140
174
|
try {
|
|
@@ -142,6 +176,14 @@ export class AgentRuntime {
|
|
|
142
176
|
|
|
143
177
|
const messageId = generateMessageId();
|
|
144
178
|
sendSSE(controller, encoder, { type: "message-start", messageId });
|
|
179
|
+
sendSSE(controller, encoder, {
|
|
180
|
+
type: "data",
|
|
181
|
+
data: {
|
|
182
|
+
inferenceMode: isLocalInferenceModel(languageModel, requestedModel)
|
|
183
|
+
? "server-local"
|
|
184
|
+
: "cloud",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
145
187
|
sendSSE(controller, encoder, { type: "text-start", id: textPartId });
|
|
146
188
|
|
|
147
189
|
await this.executeAgentLoopStreaming(
|
|
@@ -153,6 +195,7 @@ export class AgentRuntime {
|
|
|
153
195
|
textPartId,
|
|
154
196
|
toolContext,
|
|
155
197
|
modelString,
|
|
198
|
+
languageModel,
|
|
156
199
|
);
|
|
157
200
|
|
|
158
201
|
sendSSE(controller, encoder, { type: "text-end", id: textPartId });
|
|
@@ -181,17 +224,28 @@ export class AgentRuntime {
|
|
|
181
224
|
return withSpan("agent.execution_loop", async (loopSpan) => {
|
|
182
225
|
const { maxAgentSteps } = getPlatformCapabilities();
|
|
183
226
|
const maxSteps = this.computeMaxSteps(maxAgentSteps);
|
|
184
|
-
const
|
|
227
|
+
const requestedModel = modelString || this.config.model;
|
|
228
|
+
const languageModel = resolveModel(requestedModel);
|
|
185
229
|
|
|
186
230
|
const toolCalls: ToolCall[] = [];
|
|
187
231
|
const currentMessages = [...messages];
|
|
188
232
|
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
189
233
|
|
|
234
|
+
// Local models can't reliably do function calling — skip tools gracefully.
|
|
235
|
+
const isLocal = isLocalInferenceModel(languageModel, requestedModel);
|
|
236
|
+
if (isLocal && this.config.tools) {
|
|
237
|
+
logger.warn(
|
|
238
|
+
`Agent "${this.id}" has tools configured but is using local model "${requestedModel}". ` +
|
|
239
|
+
"Local models don't support tool calling — tools will be skipped. " +
|
|
240
|
+
"Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY for full tool support.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
190
244
|
for (let step = 0; step < maxSteps; step++) {
|
|
191
245
|
this.status = "thinking";
|
|
192
246
|
addSpanEvent(loopSpan, "step_start", { step });
|
|
193
247
|
|
|
194
|
-
const tools = getAvailableTools(this.config.tools);
|
|
248
|
+
const tools = isLocal ? [] : getAvailableTools(this.config.tools);
|
|
195
249
|
|
|
196
250
|
const response = await withSpan("agent.generate_text", async (span) => {
|
|
197
251
|
setSpanAttributes(span, {
|
|
@@ -350,19 +404,31 @@ export class AgentRuntime {
|
|
|
350
404
|
textPartId?: string,
|
|
351
405
|
toolContext?: Record<string, unknown>,
|
|
352
406
|
modelString?: string,
|
|
407
|
+
resolvedModel?: LanguageModel,
|
|
353
408
|
): Promise<AgentResponse> {
|
|
354
409
|
const { maxAgentSteps } = getPlatformCapabilities();
|
|
355
410
|
const maxSteps = this.computeMaxSteps(maxAgentSteps);
|
|
356
|
-
const
|
|
411
|
+
const requestedModel = modelString || this.config.model;
|
|
412
|
+
const languageModel = resolvedModel ?? resolveModel(requestedModel);
|
|
357
413
|
|
|
358
414
|
const toolCalls: ToolCall[] = [];
|
|
359
415
|
const currentMessages = [...messages];
|
|
360
416
|
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
361
417
|
|
|
418
|
+
// Local models can't reliably do function calling — skip tools gracefully.
|
|
419
|
+
const isLocalStreaming = isLocalInferenceModel(languageModel, requestedModel);
|
|
420
|
+
if (isLocalStreaming && this.config.tools) {
|
|
421
|
+
logger.warn(
|
|
422
|
+
`Agent "${this.id}" has tools configured but is using local model "${requestedModel}". ` +
|
|
423
|
+
"Local models don't support tool calling — tools will be skipped. " +
|
|
424
|
+
"Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY for full tool support.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
362
428
|
for (let step = 0; step < maxSteps; step++) {
|
|
363
429
|
sendSSE(controller, encoder, { type: "step-start" });
|
|
364
430
|
|
|
365
|
-
const tools = getAvailableTools(this.config.tools);
|
|
431
|
+
const tools = isLocalStreaming ? [] : getAvailableTools(this.config.tools);
|
|
366
432
|
const result = streamText({
|
|
367
433
|
model: languageModel,
|
|
368
434
|
system: systemPrompt,
|
|
@@ -5,39 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Client-side CSS styles for
|
|
8
|
+
* Client-side CSS styles for error display in production builds
|
|
9
9
|
*/
|
|
10
|
-
export const CLIENT_STYLES =
|
|
11
|
-
margin: 0;
|
|
12
|
-
font-family:
|
|
13
|
-
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
14
|
-
line-height: 1.5;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.loading-container {
|
|
18
|
-
display: flex;
|
|
19
|
-
justify-content: center;
|
|
20
|
-
align-items: center;
|
|
21
|
-
min-height: 100vh;
|
|
22
|
-
background: #f9fafb;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.loading-spinner {
|
|
26
|
-
width: 40px;
|
|
27
|
-
height: 40px;
|
|
28
|
-
border: 3px solid #e5e7eb;
|
|
29
|
-
border-top-color: #3b82f6;
|
|
30
|
-
border-radius: 50%;
|
|
31
|
-
animation: spin 1s linear infinite;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@keyframes spin {
|
|
35
|
-
to {
|
|
36
|
-
transform: rotate(360deg);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.error-container {
|
|
10
|
+
export const CLIENT_STYLES = `.error-container {
|
|
41
11
|
max-width: 600px;
|
|
42
12
|
margin: 2rem auto;
|
|
43
13
|
padding: 2rem;
|
|
@@ -45,42 +15,6 @@ export const CLIENT_STYLES = `body {
|
|
|
45
15
|
border: 1px solid #fcc;
|
|
46
16
|
border-radius: 8px;
|
|
47
17
|
color: #c00;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.prose {
|
|
51
|
-
max-width: 65ch;
|
|
52
|
-
margin: 0 auto;
|
|
53
|
-
padding: 2rem;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.prose h1, .prose h2, .prose h3 {
|
|
57
|
-
margin-top: 2em;
|
|
58
|
-
margin-bottom: 1em;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.prose p {
|
|
62
|
-
margin-bottom: 1.5em;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.prose code {
|
|
66
|
-
background: #f3f4f6;
|
|
67
|
-
padding: 0.2em 0.4em;
|
|
68
|
-
border-radius: 3px;
|
|
69
|
-
font-size: 0.875em;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.prose pre {
|
|
73
|
-
background: #1f2937;
|
|
74
|
-
color: #f9fafb;
|
|
75
|
-
padding: 1em;
|
|
76
|
-
border-radius: 8px;
|
|
77
|
-
overflow-x: auto;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.prose pre code {
|
|
81
|
-
background: transparent;
|
|
82
|
-
padding: 0;
|
|
83
|
-
color: inherit;
|
|
84
18
|
}`;
|
|
85
19
|
|
|
86
20
|
/**
|
package/src/src/chat/index.ts
CHANGED
|
@@ -98,7 +98,9 @@ export type { AgentTheme, ChatTheme } from "../react/components/ai/theme.js";
|
|
|
98
98
|
|
|
99
99
|
export { useChat } from "../agent/react/use-chat/index.js";
|
|
100
100
|
export type {
|
|
101
|
+
BrowserInferenceStatus,
|
|
101
102
|
DynamicToolUIPart,
|
|
103
|
+
InferenceMode,
|
|
102
104
|
OnToolCallArg,
|
|
103
105
|
ReasoningUIPart,
|
|
104
106
|
TextUIPart,
|
|
@@ -75,7 +75,8 @@ export type VeryfrontErrorData =
|
|
|
75
75
|
| { type: "file"; message: string; context?: FileContext }
|
|
76
76
|
| { type: "network"; message: string; context?: NetworkContext }
|
|
77
77
|
| { type: "permission"; message: string; context?: FileContext }
|
|
78
|
-
| { type: "not_supported"; message: string; feature?: string }
|
|
78
|
+
| { type: "not_supported"; message: string; feature?: string }
|
|
79
|
+
| { type: "no_ai_available"; message: string };
|
|
79
80
|
|
|
80
81
|
export function createError(error: VeryfrontErrorData): VeryfrontErrorData {
|
|
81
82
|
return error;
|
|
@@ -393,7 +393,11 @@ export class DenoAdapter implements RuntimeAdapter {
|
|
|
393
393
|
}
|
|
394
394
|
: handler;
|
|
395
395
|
|
|
396
|
-
|
|
396
|
+
// Access native Deno.serve via `self` to bypass dnt shim transform.
|
|
397
|
+
// dnt rewrites both `Deno.*` and `globalThis.*` to use @deno/shim-deno which lacks .serve.
|
|
398
|
+
// `self` is not shimmed by dnt and equals `globalThis` in Deno.
|
|
399
|
+
const nativeDeno = (self as unknown as Record<string, typeof dntShim.Deno>)["Deno"]!;
|
|
400
|
+
const server = nativeDeno.serve({
|
|
397
401
|
port,
|
|
398
402
|
hostname,
|
|
399
403
|
signal,
|
|
@@ -13,7 +13,9 @@ export class DenoHttpServer implements HttpServer {
|
|
|
13
13
|
|
|
14
14
|
onListen?.({ hostname, port });
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Access native Deno.serve via `self` to bypass dnt shim transform.
|
|
17
|
+
const nativeDeno = (self as unknown as Record<string, typeof dntShim.Deno>)["Deno"]!;
|
|
18
|
+
await nativeDeno.serve({ port, hostname, signal: serveSignal }, handler);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
close(): Promise<void> {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI SDK Adapter for Local Models
|
|
3
|
+
*
|
|
4
|
+
* Bridges `@huggingface/transformers` local inference to the AI SDK
|
|
5
|
+
* `LanguageModelV2` interface. This allows `streamText()` and
|
|
6
|
+
* `generateText()` to work with local models seamlessly.
|
|
7
|
+
*
|
|
8
|
+
* @module provider/local
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LanguageModel } from "ai";
|
|
12
|
+
import { generate, generateStream } from "./local-engine.js";
|
|
13
|
+
import type { ChatMessage, GenerateOptions } from "./local-engine.js";
|
|
14
|
+
import { DEFAULT_LOCAL_MODEL } from "./model-catalog.js";
|
|
15
|
+
import { serverLogger } from "../../utils/index.js";
|
|
16
|
+
import { createError, fromError, toError } from "../../errors/veryfront-error.js";
|
|
17
|
+
import { isLocalAIDisabled } from "./env.js";
|
|
18
|
+
|
|
19
|
+
const logger = serverLogger.component("local-llm");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert AI SDK LanguageModelV2 prompt format to simple ChatMessage array.
|
|
23
|
+
*
|
|
24
|
+
* The AI SDK prompt is an array of message objects with role and content arrays.
|
|
25
|
+
* We extract text content for the local model.
|
|
26
|
+
*/
|
|
27
|
+
// deno-lint-ignore no-explicit-any
|
|
28
|
+
function convertPrompt(prompt: any[]): ChatMessage[] {
|
|
29
|
+
const messages: ChatMessage[] = [];
|
|
30
|
+
|
|
31
|
+
for (const msg of prompt) {
|
|
32
|
+
const role = msg.role as "system" | "user" | "assistant" | "tool";
|
|
33
|
+
// Skip tool messages — local models don't support tool calling
|
|
34
|
+
if (role === "tool") continue;
|
|
35
|
+
|
|
36
|
+
const mappedRole = role === "system" ? "system" : role === "user" ? "user" : "assistant";
|
|
37
|
+
|
|
38
|
+
// Extract text content from content array
|
|
39
|
+
let text = "";
|
|
40
|
+
if (typeof msg.content === "string") {
|
|
41
|
+
text = msg.content;
|
|
42
|
+
} else if (Array.isArray(msg.content)) {
|
|
43
|
+
for (const part of msg.content) {
|
|
44
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
45
|
+
text += part.text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (text) {
|
|
51
|
+
messages.push({ role: mappedRole, content: text });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return messages;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a local AI SDK LanguageModel for the given model ID.
|
|
60
|
+
*
|
|
61
|
+
* The returned object implements the LanguageModelV2 interface, making it
|
|
62
|
+
* compatible with all AI SDK functions (`streamText`, `generateText`, etc.)
|
|
63
|
+
* and all VeryFront hooks (`useChat`).
|
|
64
|
+
*/
|
|
65
|
+
export function createLocalModel(modelId?: string): LanguageModel {
|
|
66
|
+
const resolvedId = modelId || DEFAULT_LOCAL_MODEL;
|
|
67
|
+
|
|
68
|
+
const model = {
|
|
69
|
+
/** Marker so ensureModelReady() can distinguish real local-engine models
|
|
70
|
+
* from mock/custom providers that happen to use provider:"local". */
|
|
71
|
+
_isVfLocalModel: true as const,
|
|
72
|
+
specificationVersion: "v2" as const,
|
|
73
|
+
provider: "local",
|
|
74
|
+
modelId: `local/${resolvedId}`,
|
|
75
|
+
|
|
76
|
+
supportedUrls: {},
|
|
77
|
+
|
|
78
|
+
async doGenerate(options: {
|
|
79
|
+
prompt: unknown[];
|
|
80
|
+
maxOutputTokens?: number;
|
|
81
|
+
temperature?: number;
|
|
82
|
+
topP?: number;
|
|
83
|
+
topK?: number;
|
|
84
|
+
stopSequences?: string[];
|
|
85
|
+
}) {
|
|
86
|
+
const messages = convertPrompt(options.prompt as unknown[]);
|
|
87
|
+
const genOptions: GenerateOptions = {
|
|
88
|
+
maxNewTokens: options.maxOutputTokens ?? 512,
|
|
89
|
+
temperature: options.temperature ?? 0.7,
|
|
90
|
+
topP: options.topP,
|
|
91
|
+
topK: options.topK,
|
|
92
|
+
stopSequences: options.stopSequences,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
logger.debug(`[local] doGenerate: ${messages.length} messages → ${resolvedId}`);
|
|
96
|
+
|
|
97
|
+
const text = await generate(resolvedId, messages, genOptions);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text" as const, text }],
|
|
101
|
+
finishReason: "stop" as const,
|
|
102
|
+
usage: {
|
|
103
|
+
inputTokens: undefined,
|
|
104
|
+
outputTokens: undefined,
|
|
105
|
+
totalTokens: undefined,
|
|
106
|
+
},
|
|
107
|
+
warnings: [],
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async doStream(options: {
|
|
112
|
+
prompt: unknown[];
|
|
113
|
+
maxOutputTokens?: number;
|
|
114
|
+
temperature?: number;
|
|
115
|
+
topP?: number;
|
|
116
|
+
topK?: number;
|
|
117
|
+
stopSequences?: string[];
|
|
118
|
+
}) {
|
|
119
|
+
// Eagerly check if local AI is disabled — must throw before creating the
|
|
120
|
+
// ReadableStream, otherwise the 200 response headers are already committed.
|
|
121
|
+
// Note: getTransformers() in local-engine.ts also checks this, but we need
|
|
122
|
+
// the check here too because doStream creates a ReadableStream wrapper and
|
|
123
|
+
// errors inside it would be swallowed as in-band stream errors.
|
|
124
|
+
if (isLocalAIDisabled()) {
|
|
125
|
+
throw toError(
|
|
126
|
+
createError({
|
|
127
|
+
type: "no_ai_available",
|
|
128
|
+
message: "Local AI disabled via VERYFRONT_DISABLE_LOCAL_AI environment variable.",
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const messages = convertPrompt(options.prompt as unknown[]);
|
|
134
|
+
const genOptions: GenerateOptions = {
|
|
135
|
+
maxNewTokens: options.maxOutputTokens ?? 512,
|
|
136
|
+
temperature: options.temperature ?? 0.7,
|
|
137
|
+
topP: options.topP,
|
|
138
|
+
topK: options.topK,
|
|
139
|
+
stopSequences: options.stopSequences,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
logger.debug(`[local] doStream: ${messages.length} messages → ${resolvedId}`);
|
|
143
|
+
|
|
144
|
+
const textId = `text-${Date.now()}`;
|
|
145
|
+
|
|
146
|
+
const stream = new ReadableStream({
|
|
147
|
+
async start(controller) {
|
|
148
|
+
try {
|
|
149
|
+
// Emit stream-start
|
|
150
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
151
|
+
|
|
152
|
+
// Emit response metadata
|
|
153
|
+
controller.enqueue({
|
|
154
|
+
type: "response-metadata",
|
|
155
|
+
id: `local-${Date.now()}`,
|
|
156
|
+
timestamp: new Date(),
|
|
157
|
+
modelId: `local/${resolvedId}`,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Emit text-start
|
|
161
|
+
controller.enqueue({ type: "text-start", id: textId });
|
|
162
|
+
|
|
163
|
+
// Stream tokens
|
|
164
|
+
for await (const token of generateStream(resolvedId, messages, genOptions)) {
|
|
165
|
+
controller.enqueue({
|
|
166
|
+
type: "text-delta",
|
|
167
|
+
id: textId,
|
|
168
|
+
delta: token,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Emit text-end
|
|
173
|
+
controller.enqueue({ type: "text-end", id: textId });
|
|
174
|
+
|
|
175
|
+
// Emit finish
|
|
176
|
+
controller.enqueue({
|
|
177
|
+
type: "finish",
|
|
178
|
+
finishReason: "stop",
|
|
179
|
+
usage: {
|
|
180
|
+
inputTokens: undefined,
|
|
181
|
+
outputTokens: undefined,
|
|
182
|
+
totalTokens: undefined,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
controller.close();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// Let no_ai_available propagate — the chat handler needs it
|
|
189
|
+
// for a proper 503 response instead of a 200 with in-band error.
|
|
190
|
+
const vfError = fromError(error);
|
|
191
|
+
if (vfError?.type === "no_ai_available") throw error;
|
|
192
|
+
|
|
193
|
+
controller.enqueue({
|
|
194
|
+
type: "error",
|
|
195
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
196
|
+
});
|
|
197
|
+
controller.close();
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return { stream };
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return model as LanguageModel;
|
|
207
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform environment helpers for local AI provider.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts Deno/Node env access so all local-AI checks go through
|
|
5
|
+
* a single function — no duplicated `(globalThis as any).Deno?.env` patterns.
|
|
6
|
+
*
|
|
7
|
+
* @module provider/local
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check whether local AI is explicitly disabled via environment variable.
|
|
12
|
+
* Works in Deno, Node, and compiled binaries.
|
|
13
|
+
*/
|
|
14
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
15
|
+
|
|
16
|
+
export function isLocalAIDisabled(): boolean {
|
|
17
|
+
// deno-lint-ignore no-explicit-any
|
|
18
|
+
const denoVal = (dntShim.dntGlobalThis as any).Deno?.env?.get?.("VERYFRONT_DISABLE_LOCAL_AI");
|
|
19
|
+
if (denoVal === "1") return true;
|
|
20
|
+
|
|
21
|
+
if (typeof process !== "undefined" && process.env?.VERYFRONT_DISABLE_LOCAL_AI === "1") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|