@larkup/workspace-server 0.0.0-stage → 0.2.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 +176 -0
- package/README.md +2 -2
- package/dist-types/app/api/chat/feedback/route.d.ts +4 -0
- package/dist-types/app/api/chat/feedback/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/route.d.ts +3 -0
- package/dist-types/app/api/chat/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/tools.d.ts +67 -0
- package/dist-types/app/api/chat/tools.d.ts.map +1 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts +3 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts.map +1 -0
- package/dist-types/app/api/config/verify/route.d.ts +3 -0
- package/dist-types/app/api/config/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/describe-image/route.d.ts +4 -0
- package/dist-types/app/api/describe-image/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts +9 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts +2 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts.map +1 -0
- package/dist-types/app/api/document/sign/route.d.ts +2 -0
- package/dist-types/app/api/document/sign/route.d.ts.map +1 -0
- package/dist-types/app/api/files/remote/route.d.ts +5 -0
- package/dist-types/app/api/files/remote/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts +9 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts +8 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/route.d.ts +4 -0
- package/dist-types/app/api/mcp/route.d.ts.map +1 -0
- package/dist-types/app/api/parse-file/route.d.ts +6 -0
- package/dist-types/app/api/parse-file/route.d.ts.map +1 -0
- package/dist-types/app/api/pdf-page/route.d.ts +4 -0
- package/dist-types/app/api/pdf-page/route.d.ts.map +1 -0
- package/dist-types/app/api/proxy/verify/route.d.ts +4 -0
- package/dist-types/app/api/proxy/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/search/bing/route.d.ts +5 -0
- package/dist-types/app/api/search/bing/route.d.ts.map +1 -0
- package/dist-types/app/api/search/brave/route.d.ts +5 -0
- package/dist-types/app/api/search/brave/route.d.ts.map +1 -0
- package/dist-types/app/api/search/exa/route.d.ts +5 -0
- package/dist-types/app/api/search/exa/route.d.ts.map +1 -0
- package/dist-types/app/api/search/google/route.d.ts +30 -0
- package/dist-types/app/api/search/google/route.d.ts.map +1 -0
- package/dist-types/app/api/search/route.d.ts +7 -0
- package/dist-types/app/api/search/route.d.ts.map +1 -0
- package/dist-types/app/api/search/tavily/route.d.ts +5 -0
- package/dist-types/app/api/search/tavily/route.d.ts.map +1 -0
- package/dist-types/app/api/search/verify/route.d.ts +4 -0
- package/dist-types/app/api/search/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/tabular/route.d.ts +9 -0
- package/dist-types/app/api/tabular/route.d.ts.map +1 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts +10 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts.map +1 -0
- package/dist-types/lib/chat/conversation-memory.d.ts +80 -0
- package/dist-types/lib/chat/conversation-memory.d.ts.map +1 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts +7 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts.map +1 -0
- package/dist-types/lib/chat/image-description.d.ts +10 -0
- package/dist-types/lib/chat/image-description.d.ts.map +1 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts +14 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/media-source-routing.d.ts +53 -0
- package/dist-types/lib/chat/media-source-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/message-input.d.ts +8 -0
- package/dist-types/lib/chat/message-input.d.ts.map +1 -0
- package/dist-types/lib/chat/model-provider.d.ts +23 -0
- package/dist-types/lib/chat/model-provider.d.ts.map +1 -0
- package/dist-types/lib/chat/response-style.d.ts +2 -0
- package/dist-types/lib/chat/response-style.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts +32 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts +39 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts +15 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts +12 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-routing.d.ts +41 -0
- package/dist-types/lib/chat/tabular-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts +33 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts +31 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts +11 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-context.d.ts +72 -0
- package/dist-types/lib/chat/tool-context.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-registry.d.ts +3 -0
- package/dist-types/lib/chat/tool-registry.d.ts.map +1 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts +33 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/visual-routing.d.ts +53 -0
- package/dist-types/lib/chat/visual-routing.d.ts.map +1 -0
- package/dist-types/lib/enterprise-client.d.ts +31 -0
- package/dist-types/lib/enterprise-client.d.ts.map +1 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts +7 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts.map +1 -0
- package/dist-types/lib/media/knowledge.d.ts +47 -0
- package/dist-types/lib/media/knowledge.d.ts.map +1 -0
- package/dist-types/lib/media/pdf-inspection.d.ts +30 -0
- package/dist-types/lib/media/pdf-inspection.d.ts.map +1 -0
- package/dist-types/lib/media/source-identity.d.ts +12 -0
- package/dist-types/lib/media/source-identity.d.ts.map +1 -0
- package/dist-types/lib/media/source-utils.d.ts +157 -0
- package/dist-types/lib/media/source-utils.d.ts.map +1 -0
- package/dist-types/lib/media/video/quick-look.d.ts +60 -0
- package/dist-types/lib/media/video/quick-look.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts +521 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts +32 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts.map +1 -0
- package/dist-types/lib/remote-files.d.ts +12 -0
- package/dist-types/lib/remote-files.d.ts.map +1 -0
- package/dist-types/lib/search/provider-verification.d.ts +5 -0
- package/dist-types/lib/search/provider-verification.d.ts.map +1 -0
- package/dist-types/runtime-host.d.ts +20 -0
- package/dist-types/runtime-host.d.ts.map +1 -0
- package/package.json +338 -3
- package/src/app/api/chat/feedback/route.ts +77 -0
- package/src/app/api/chat/route.ts +2277 -0
- package/src/app/api/chat/tools.ts +3563 -0
- package/src/app/api/config/test-embedding/route.ts +39 -0
- package/src/app/api/config/verify/route.ts +309 -0
- package/src/app/api/describe-image/route.ts +108 -0
- package/src/app/api/doc-editor/edit/route.ts +87 -0
- package/src/app/api/doc-editor/export/route.ts +32 -0
- package/src/app/api/doc-editor/parse/route.ts +74 -0
- package/src/app/api/doc-editor/restore/route.ts +70 -0
- package/src/app/api/document/sign/route.ts +55 -0
- package/src/app/api/files/remote/route.ts +32 -0
- package/src/app/api/mcp/[connectionId]/route.ts +29 -0
- package/src/app/api/mcp/[connectionId]/test/route.ts +13 -0
- package/src/app/api/mcp/route.ts +22 -0
- package/src/app/api/parse-file/route.ts +86 -0
- package/src/app/api/pdf-page/route.ts +40 -0
- package/src/app/api/proxy/verify/route.ts +78 -0
- package/src/app/api/search/bing/route.ts +86 -0
- package/src/app/api/search/brave/route.ts +88 -0
- package/src/app/api/search/exa/route.ts +100 -0
- package/src/app/api/search/google/route.ts +147 -0
- package/src/app/api/search/route.ts +28 -0
- package/src/app/api/search/tavily/route.ts +93 -0
- package/src/app/api/search/verify/route.ts +27 -0
- package/src/app/api/tabular/route.ts +68 -0
- package/src/lib/chat/assistant-source-scope.ts +32 -0
- package/src/lib/chat/conversation-memory.ts +621 -0
- package/src/lib/chat/gateway-fallbacks.ts +12 -0
- package/src/lib/chat/image-description.ts +21 -0
- package/src/lib/chat/media-retrieval-routing.ts +37 -0
- package/src/lib/chat/media-source-routing.ts +100 -0
- package/src/lib/chat/message-input.ts +48 -0
- package/src/lib/chat/model-provider.ts +157 -0
- package/src/lib/chat/response-style.ts +3 -0
- package/src/lib/chat/retrieval-ranking.ts +126 -0
- package/src/lib/chat/retrieval-routing.ts +221 -0
- package/src/lib/chat/tabular-dataset-routing.ts +93 -0
- package/src/lib/chat/tabular-query-plan.ts +195 -0
- package/src/lib/chat/tabular-routing.ts +122 -0
- package/src/lib/chat/tabular-sandbox-files.ts +78 -0
- package/src/lib/chat/tabular-visualization.ts +165 -0
- package/src/lib/chat/tool-call-repair.ts +84 -0
- package/src/lib/chat/tool-context.ts +1222 -0
- package/src/lib/chat/tool-registry.ts +6 -0
- package/src/lib/chat/video-rag-routing.ts +149 -0
- package/src/lib/chat/visual-routing.ts +225 -0
- package/src/lib/enterprise-client.ts +96 -0
- package/src/lib/marketplace/tool-runtime-config.ts +155 -0
- package/src/lib/media/knowledge.ts +489 -0
- package/src/lib/media/pdf-inspection.ts +280 -0
- package/src/lib/media/source-identity.ts +50 -0
- package/src/lib/media/source-utils.ts +1156 -0
- package/src/lib/media/video/quick-look.ts +368 -0
- package/src/lib/media/video-intelligence-adapter.ts +1946 -0
- package/src/lib/media/video-intelligence-connection.ts +151 -0
- package/src/lib/remote-files.ts +182 -0
- package/src/lib/search/provider-verification.ts +95 -0
- package/src/runtime-host.ts +31 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
|
+
import { embed } from 'ai';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'nodejs';
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
let body: { baseUrl: string; apiKey?: string; modelName: string };
|
|
8
|
+
try {
|
|
9
|
+
body = await request.json();
|
|
10
|
+
} catch {
|
|
11
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { baseUrl, apiKey, modelName } = body;
|
|
15
|
+
|
|
16
|
+
if (!baseUrl || !modelName) {
|
|
17
|
+
return Response.json(
|
|
18
|
+
{ error: 'Missing required fields: baseUrl and modelName' },
|
|
19
|
+
{ status: 400 },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const customProvider = createOpenAI({
|
|
25
|
+
baseURL: baseUrl,
|
|
26
|
+
apiKey: apiKey || 'empty', // some backends require any string
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const model = customProvider.embedding(modelName);
|
|
30
|
+
const { embedding } = await embed({ model, value: 'test connection' });
|
|
31
|
+
|
|
32
|
+
return Response.json({ success: true, dimensions: embedding.length });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return Response.json(
|
|
35
|
+
{ error: error instanceof Error ? error.message : 'Embedding connection failed' },
|
|
36
|
+
{ status: 400 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { embed, generateText } from 'ai';
|
|
2
|
+
import { getAIModel } from '@larkup/core/embeddings/providers';
|
|
3
|
+
import { getEmbeddingModel } from '@larkup/core/embeddings/registry';
|
|
4
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
5
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
6
|
+
import { createCohere } from '@ai-sdk/cohere';
|
|
7
|
+
import { createMistral } from '@ai-sdk/mistral';
|
|
8
|
+
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
9
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
10
|
+
import { createGateway } from '@ai-sdk/gateway';
|
|
11
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
12
|
+
import type { CustomModelConfig } from '@larkup/core/types';
|
|
13
|
+
import { formatErrorMessage } from '@larkup/core/error-formatter';
|
|
14
|
+
import { normalizeNativeChatModelId } from '@larkup/core/chat-models/registry';
|
|
15
|
+
|
|
16
|
+
export const runtime = 'nodejs';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CHAT_MODELS: Record<string, string> = {
|
|
19
|
+
openai: 'gpt-4o-mini',
|
|
20
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
21
|
+
google: 'gemini-3.6-flash',
|
|
22
|
+
mistral: 'mistral-small-latest',
|
|
23
|
+
deepseek: 'deepseek-chat',
|
|
24
|
+
cohere: 'command-r',
|
|
25
|
+
vercel_ai_gateway: 'openai/gpt-4o-mini',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_EMBEDDING_MODELS: Record<string, string> = {
|
|
29
|
+
openai: 'openai/text-embedding-3-small',
|
|
30
|
+
google: 'google/gemini-embedding-001',
|
|
31
|
+
cohere: 'cohere/embed-english-v3.0',
|
|
32
|
+
mistral: 'mistral/mistral-embed',
|
|
33
|
+
vercel_ai_gateway: 'openai/text-embedding-3-small',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function createChatModel(
|
|
37
|
+
provider: string,
|
|
38
|
+
modelId: string,
|
|
39
|
+
apiKey?: string,
|
|
40
|
+
customChatModels?: CustomModelConfig[],
|
|
41
|
+
) {
|
|
42
|
+
if (modelId.startsWith('custom:')) {
|
|
43
|
+
const customName = modelId.slice('custom:'.length);
|
|
44
|
+
const custom = (customChatModels ?? []).find((m) => m.modelName === customName);
|
|
45
|
+
if (custom) {
|
|
46
|
+
const customProvider = createOpenAICompatible({
|
|
47
|
+
name: 'custom_chat_provider',
|
|
48
|
+
baseURL: custom.baseUrl,
|
|
49
|
+
apiKey: custom.apiKey || apiKey || undefined,
|
|
50
|
+
});
|
|
51
|
+
return customProvider(custom.modelName);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const modelName = modelId.includes('/') ? modelId.split('/').slice(1).join('/') : modelId;
|
|
56
|
+
|
|
57
|
+
switch (provider) {
|
|
58
|
+
case 'google':
|
|
59
|
+
return createGoogleGenerativeAI({ apiKey })(modelName);
|
|
60
|
+
case 'cohere':
|
|
61
|
+
return createCohere({ apiKey })(modelName);
|
|
62
|
+
case 'mistral':
|
|
63
|
+
return createMistral({ apiKey })(modelName);
|
|
64
|
+
case 'deepseek':
|
|
65
|
+
return createDeepSeek({ apiKey })(modelName);
|
|
66
|
+
case 'anthropic':
|
|
67
|
+
return createAnthropic({ apiKey })(modelName);
|
|
68
|
+
case 'openai':
|
|
69
|
+
return createOpenAI({ apiKey })(modelName);
|
|
70
|
+
case 'vercel_ai_gateway':
|
|
71
|
+
return createGateway({ apiKey })(modelId);
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unsupported chat provider "${provider}".`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function POST(request: Request) {
|
|
78
|
+
const body = await request.json();
|
|
79
|
+
const {
|
|
80
|
+
embeddingProvider,
|
|
81
|
+
embeddingApiKey,
|
|
82
|
+
embeddingModelId,
|
|
83
|
+
chatProvider,
|
|
84
|
+
chatApiKey,
|
|
85
|
+
chatModelId,
|
|
86
|
+
customChatModels,
|
|
87
|
+
customEmbeddings,
|
|
88
|
+
audioProvider,
|
|
89
|
+
audioApiKey,
|
|
90
|
+
audioModelId,
|
|
91
|
+
} = body;
|
|
92
|
+
|
|
93
|
+
if (!embeddingProvider && !chatProvider && !audioProvider) {
|
|
94
|
+
return Response.json({ error: 'No provider fields to verify' }, { status: 400 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify embedding.
|
|
98
|
+
if (embeddingProvider) {
|
|
99
|
+
try {
|
|
100
|
+
const resolvedEmbeddingProvider =
|
|
101
|
+
embeddingProvider === 'vercel_ai_gateway' ? 'vercel_ai_gateway' : embeddingProvider;
|
|
102
|
+
|
|
103
|
+
let resolvedEmbeddingModelId = embeddingModelId;
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
resolvedEmbeddingModelId &&
|
|
107
|
+
resolvedEmbeddingProvider !== 'vercel_ai_gateway' &&
|
|
108
|
+
resolvedEmbeddingProvider !== 'custom'
|
|
109
|
+
) {
|
|
110
|
+
const modelInfo =
|
|
111
|
+
getEmbeddingModel(resolvedEmbeddingModelId) ||
|
|
112
|
+
getEmbeddingModel(`${resolvedEmbeddingProvider}/${resolvedEmbeddingModelId}`);
|
|
113
|
+
|
|
114
|
+
if (!modelInfo || modelInfo.provider !== resolvedEmbeddingProvider) {
|
|
115
|
+
resolvedEmbeddingModelId = ''; // mismatch or unknown, force fallback
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!resolvedEmbeddingModelId) {
|
|
120
|
+
resolvedEmbeddingModelId =
|
|
121
|
+
DEFAULT_EMBEDDING_MODELS[resolvedEmbeddingProvider] || DEFAULT_EMBEDDING_MODELS['openai'];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Handle custom embedding models
|
|
125
|
+
if (
|
|
126
|
+
resolvedEmbeddingProvider === 'custom' &&
|
|
127
|
+
customEmbeddings &&
|
|
128
|
+
customEmbeddings.length > 0
|
|
129
|
+
) {
|
|
130
|
+
resolvedEmbeddingModelId = `custom:${customEmbeddings[0].modelName}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const configMock = {
|
|
134
|
+
embeddingProvider,
|
|
135
|
+
embeddingApiKey,
|
|
136
|
+
embeddingModelId: resolvedEmbeddingModelId,
|
|
137
|
+
customEmbeddings,
|
|
138
|
+
} as any;
|
|
139
|
+
const model = getAIModel(configMock);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await embed({ model, value: 'test' });
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
const msg = err.message || '';
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
msg.includes('not found') ||
|
|
148
|
+
msg.includes('not available') ||
|
|
149
|
+
msg.includes('no longer available') ||
|
|
150
|
+
msg.includes('does not exist')
|
|
151
|
+
) {
|
|
152
|
+
// For Google, try to list available models
|
|
153
|
+
if (resolvedEmbeddingProvider === 'google') {
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(
|
|
156
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${embeddingApiKey}`,
|
|
157
|
+
);
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
const embedModels = (data.models || [])
|
|
160
|
+
.filter((m: any) => m.supportedGenerationMethods?.includes('embedContent'))
|
|
161
|
+
.map((m: any) => m.name);
|
|
162
|
+
return Response.json(
|
|
163
|
+
{
|
|
164
|
+
error: `Embedding model "${resolvedEmbeddingModelId}" is not available for your API key. Available models: ${embedModels.join(
|
|
165
|
+
', ',
|
|
166
|
+
)}`,
|
|
167
|
+
},
|
|
168
|
+
{ status: 400 },
|
|
169
|
+
);
|
|
170
|
+
} catch {
|
|
171
|
+
// ignore listing error
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Response.json(
|
|
176
|
+
{
|
|
177
|
+
error: `Embedding model "${resolvedEmbeddingModelId}" is not available. Please select a different model or check your API key.`,
|
|
178
|
+
},
|
|
179
|
+
{ status: 400 },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Response.json({ error: formatErrorMessage(err) }, { status: 400 });
|
|
184
|
+
}
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
return Response.json({ error: formatErrorMessage(err) }, { status: 400 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Verify chat.
|
|
191
|
+
if (chatProvider) {
|
|
192
|
+
try {
|
|
193
|
+
const resolvedProvider =
|
|
194
|
+
chatProvider === 'vercel_ai_gateway' ? 'vercel_ai_gateway' : chatProvider;
|
|
195
|
+
|
|
196
|
+
let modelId: string;
|
|
197
|
+
if (resolvedProvider === 'custom' && customChatModels && customChatModels.length > 0) {
|
|
198
|
+
modelId = `custom:${customChatModels[0].modelName}`;
|
|
199
|
+
} else if (chatModelId) {
|
|
200
|
+
modelId = chatModelId;
|
|
201
|
+
} else {
|
|
202
|
+
modelId =
|
|
203
|
+
resolvedProvider === 'vercel_ai_gateway'
|
|
204
|
+
? DEFAULT_CHAT_MODELS['vercel_ai_gateway']
|
|
205
|
+
: `${resolvedProvider}/${DEFAULT_CHAT_MODELS[resolvedProvider] || 'gpt-4o-mini'}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
modelId = normalizeNativeChatModelId(resolvedProvider, modelId) || modelId;
|
|
209
|
+
const model = createChatModel(resolvedProvider, modelId, chatApiKey, customChatModels) as any;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await generateText({ model, prompt: 'hi', maxOutputTokens: 16 });
|
|
213
|
+
} catch (err: any) {
|
|
214
|
+
const msg = err.message || '';
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
msg.includes('not found') ||
|
|
218
|
+
msg.includes('not available') ||
|
|
219
|
+
msg.includes('no longer available') ||
|
|
220
|
+
msg.includes('does not exist') ||
|
|
221
|
+
msg.includes('is not supported for generateContent')
|
|
222
|
+
) {
|
|
223
|
+
if (chatModelId) {
|
|
224
|
+
return Response.json(
|
|
225
|
+
{
|
|
226
|
+
error: `Chat model "${modelId}" is not available for your API key. Please select a different model.`,
|
|
227
|
+
},
|
|
228
|
+
{ status: 400 },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return Response.json(
|
|
233
|
+
{
|
|
234
|
+
error: `The default test model for ${chatProvider} could not be verified. Your API key might only support specific models. Please select a model manually.`,
|
|
235
|
+
},
|
|
236
|
+
{ status: 400 },
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Authentication / quota errors
|
|
241
|
+
if (
|
|
242
|
+
msg.includes('API key') ||
|
|
243
|
+
msg.includes('authentication') ||
|
|
244
|
+
msg.includes('unauthorized') ||
|
|
245
|
+
msg.includes('401')
|
|
246
|
+
) {
|
|
247
|
+
return Response.json(
|
|
248
|
+
{
|
|
249
|
+
error: `Chat API key is invalid or unauthorized for ${chatProvider}. Please check your key.`,
|
|
250
|
+
},
|
|
251
|
+
{ status: 400 },
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return Response.json({ error: formatErrorMessage(err) }, { status: 400 });
|
|
256
|
+
}
|
|
257
|
+
} catch (err: any) {
|
|
258
|
+
return Response.json({ error: formatErrorMessage(err) }, { status: 400 });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Verify audio.
|
|
263
|
+
if (audioProvider && audioProvider !== 'local') {
|
|
264
|
+
try {
|
|
265
|
+
let url = '';
|
|
266
|
+
let headers: Record<string, string> = {
|
|
267
|
+
Authorization: `Bearer ${audioApiKey}`,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (audioProvider === 'openai') {
|
|
271
|
+
url = 'https://api.openai.com/v1/audio/transcriptions';
|
|
272
|
+
} else if (audioProvider === 'groq') {
|
|
273
|
+
url = 'https://api.groq.com/openai/v1/audio/transcriptions';
|
|
274
|
+
} else if (audioProvider === 'deepgram') {
|
|
275
|
+
url = 'https://api.deepgram.com/v1/listen';
|
|
276
|
+
headers = { Authorization: `Token ${audioApiKey}` };
|
|
277
|
+
} else if (audioProvider === 'assemblyai') {
|
|
278
|
+
url = 'https://api.assemblyai.com/v2/transcript';
|
|
279
|
+
headers = { Authorization: audioApiKey };
|
|
280
|
+
} else if (audioProvider === 'elevenlabs') {
|
|
281
|
+
url = 'https://api.elevenlabs.io/v1/speech-to-text';
|
|
282
|
+
headers = { 'xi-api-key': audioApiKey };
|
|
283
|
+
} else if (audioProvider === 'azure openai') {
|
|
284
|
+
return Response.json({ success: true }); // Skip verify for now
|
|
285
|
+
} else if (audioProvider === 'rev.ai') {
|
|
286
|
+
url = 'https://api.rev.ai/speechtotext/v1/jobs';
|
|
287
|
+
} else if (audioProvider === 'fal') {
|
|
288
|
+
return Response.json({ success: true }); // Skip verify for now
|
|
289
|
+
} else if (audioProvider === 'google') {
|
|
290
|
+
return Response.json({ success: true }); // Skip verify for now
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (url) {
|
|
294
|
+
const verifyRes = await fetch(url, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (verifyRes.status === 401 || verifyRes.status === 403) {
|
|
300
|
+
return Response.json({ error: `Invalid API Key for ${audioProvider}` }, { status: 400 });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (err: any) {
|
|
304
|
+
return Response.json({ error: `Audio verification failed: ${err.message}` }, { status: 400 });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return Response.json({ success: true });
|
|
309
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { readConfig } from '@larkup/core/config-store';
|
|
3
|
+
import { getModelsByType } from '@larkup/core/models-cache';
|
|
4
|
+
import { runWithProject } from '@larkup/core/project-store';
|
|
5
|
+
import {
|
|
6
|
+
createImageDescriptionSignal,
|
|
7
|
+
isImageDescriptionAbort,
|
|
8
|
+
} from '@larkup/workspace-server/lib/chat/image-description';
|
|
9
|
+
import {
|
|
10
|
+
createChatModel,
|
|
11
|
+
resolveConfiguredVisionModel,
|
|
12
|
+
} from '@larkup/workspace-server/lib/chat/model-provider';
|
|
13
|
+
|
|
14
|
+
export const runtime = 'nodejs';
|
|
15
|
+
export const dynamic = 'force-dynamic';
|
|
16
|
+
|
|
17
|
+
export async function POST(req: Request) {
|
|
18
|
+
const serverId = new URL(req.url).searchParams.get('serverId');
|
|
19
|
+
const describe = () => describeImage(req);
|
|
20
|
+
return serverId ? runWithProject(serverId, describe) : describe();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function describeImage(req: Request) {
|
|
24
|
+
try {
|
|
25
|
+
const { base64, base64Images, prompt } = (await req.json()) as {
|
|
26
|
+
base64?: string;
|
|
27
|
+
base64Images?: string[];
|
|
28
|
+
prompt?: string;
|
|
29
|
+
};
|
|
30
|
+
const images = (base64Images?.length ? base64Images : base64 ? [base64] : []).slice(0, 6);
|
|
31
|
+
if (images.length === 0) {
|
|
32
|
+
return Response.json({ error: 'Missing image data' }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config = await readConfig();
|
|
36
|
+
const models = await getModelsByType('language');
|
|
37
|
+
const resolved = resolveConfiguredVisionModel(config, models);
|
|
38
|
+
const model = createChatModel(
|
|
39
|
+
resolved.provider,
|
|
40
|
+
resolved.modelId,
|
|
41
|
+
resolved.apiKey,
|
|
42
|
+
config.customVisionModels,
|
|
43
|
+
) as any;
|
|
44
|
+
|
|
45
|
+
const defaultPrompt =
|
|
46
|
+
'Provide a very brief, high-level summary of what this image contains (e.g., "A database ER diagram about films and actors", "A bar chart showing quarterly revenue"). Do NOT extract all text or exhaustively describe relationships. Keep it under 2 sentences.';
|
|
47
|
+
|
|
48
|
+
const { text, usage } = await generateText({
|
|
49
|
+
model,
|
|
50
|
+
// Do not use req.signal here. Media jobs and PDF uploads can retry or
|
|
51
|
+
// complete their outer request while the vision provider is backing off,
|
|
52
|
+
// which used to abort the AI SDK retry delay with "Delay was aborted".
|
|
53
|
+
abortSignal: createImageDescriptionSignal(),
|
|
54
|
+
messages: [
|
|
55
|
+
{
|
|
56
|
+
role: 'user',
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: prompt || defaultPrompt,
|
|
61
|
+
},
|
|
62
|
+
...images.map((image) => {
|
|
63
|
+
const data = image.startsWith('data:') ? image : `data:image/jpeg;base64,${image}`;
|
|
64
|
+
const mediaType = data.match(/^data:([^;,]+)/)?.[1] || 'image/jpeg';
|
|
65
|
+
return {
|
|
66
|
+
// AI SDK 6 uses file parts for every binary attachment. The
|
|
67
|
+
// legacy `image` part emits a deprecation warning on every
|
|
68
|
+
// PDF/image analysis request.
|
|
69
|
+
type: 'file' as const,
|
|
70
|
+
mediaType,
|
|
71
|
+
data,
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { estimateCost, trackUsageEvent } = await import('@larkup/core/analytics-store');
|
|
80
|
+
await trackUsageEvent({
|
|
81
|
+
type: 'media_processing',
|
|
82
|
+
mediaType: 'image',
|
|
83
|
+
modelId: resolved.modelId,
|
|
84
|
+
provider: resolved.provider,
|
|
85
|
+
promptTokens: usage.inputTokens ?? 0,
|
|
86
|
+
completionTokens: usage.outputTokens ?? 0,
|
|
87
|
+
totalTokens: usage.totalTokens ?? 0,
|
|
88
|
+
estimatedCost: estimateCost(
|
|
89
|
+
resolved.modelId,
|
|
90
|
+
usage.inputTokens ?? 0,
|
|
91
|
+
usage.outputTokens ?? 0,
|
|
92
|
+
),
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return Response.json({ description: text });
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
if (isImageDescriptionAbort(err)) {
|
|
99
|
+
console.warn('Image description timed out before the vision provider responded.');
|
|
100
|
+
return Response.json(
|
|
101
|
+
{ error: 'Image description timed out. The image can be retried.' },
|
|
102
|
+
{ status: 504 },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
console.error('Image description failed:', err);
|
|
106
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { applyFieldEdits, applyContentEdits, getSession } from '@larkup/tool-doc-editor';
|
|
2
|
+
import type { FieldEdit, ContentEdit } from '@larkup/tool-doc-editor';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'nodejs';
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST /api/doc-editor/edit
|
|
9
|
+
*
|
|
10
|
+
* Applies edits (field fills or content changes) to a document session.
|
|
11
|
+
* Returns the updated parsed document and a flag for live preview refresh.
|
|
12
|
+
*/
|
|
13
|
+
export async function POST(req: Request) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const { sessionId, fieldEdits, contentEdits } = body as {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
fieldEdits?: FieldEdit[];
|
|
19
|
+
contentEdits?: ContentEdit[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (!sessionId) {
|
|
23
|
+
return Response.json({ error: 'sessionId required' }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const session = getSession(sessionId);
|
|
27
|
+
if (!session) {
|
|
28
|
+
return Response.json({ error: 'Session not found' }, { status: 404 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Apply field edits if provided
|
|
32
|
+
if (fieldEdits && fieldEdits.length > 0) {
|
|
33
|
+
const result = await applyFieldEdits(sessionId, fieldEdits);
|
|
34
|
+
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
return Response.json(
|
|
37
|
+
{ error: result.error, fields: session.parsed.fields },
|
|
38
|
+
{ status: 500 },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Response.json({
|
|
43
|
+
success: true,
|
|
44
|
+
updatedFields: result.updatedFields,
|
|
45
|
+
fields: result.parsed.fields,
|
|
46
|
+
pages: result.parsed.pages.map((p) => ({
|
|
47
|
+
index: p.index,
|
|
48
|
+
text: p.text.slice(0, 2000),
|
|
49
|
+
html: p.html,
|
|
50
|
+
fields: p.fields,
|
|
51
|
+
dimensions: p.dimensions,
|
|
52
|
+
})),
|
|
53
|
+
totalPages: result.parsed.totalPages,
|
|
54
|
+
// Include base64 for live preview
|
|
55
|
+
fileBase64: result.fileBase64,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Apply content edits if provided
|
|
60
|
+
if (contentEdits && contentEdits.length > 0) {
|
|
61
|
+
const result = await applyContentEdits(sessionId, contentEdits);
|
|
62
|
+
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
return Response.json({ error: result.error }, { status: 500 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Response.json({
|
|
68
|
+
success: true,
|
|
69
|
+
updatedFields: result.updatedFields,
|
|
70
|
+
pages: result.parsed.pages.map((p) => ({
|
|
71
|
+
index: p.index,
|
|
72
|
+
text: p.text.slice(0, 2000),
|
|
73
|
+
html: p.html,
|
|
74
|
+
fields: p.fields,
|
|
75
|
+
dimensions: p.dimensions,
|
|
76
|
+
})),
|
|
77
|
+
totalPages: result.parsed.totalPages,
|
|
78
|
+
fileBase64: result.fileBase64,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Response.json({ error: 'No edits provided' }, { status: 400 });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : 'Edit failed';
|
|
85
|
+
return Response.json({ error: message }, { status: 500 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { exportDocument } from '@larkup/tool-doc-editor';
|
|
2
|
+
|
|
3
|
+
export const runtime = 'nodejs';
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/doc-editor/export?sessionId=xxx
|
|
8
|
+
*
|
|
9
|
+
* Downloads the current (possibly modified) document as a binary file.
|
|
10
|
+
*/
|
|
11
|
+
export async function GET(req: Request) {
|
|
12
|
+
const url = new URL(req.url);
|
|
13
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
14
|
+
|
|
15
|
+
if (!sessionId) {
|
|
16
|
+
return Response.json({ error: 'sessionId required' }, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = exportDocument(sessionId);
|
|
20
|
+
if (!result) {
|
|
21
|
+
return Response.json({ error: 'Session not found' }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return new Response(new Uint8Array(result.buffer), {
|
|
25
|
+
status: 200,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': result.mimeType,
|
|
28
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(result.fileName)}"`,
|
|
29
|
+
'Content-Length': String(result.buffer.length),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createSession, enrichPDFWithText, getSession } from '@larkup/tool-doc-editor';
|
|
2
|
+
import { PDFParse } from 'pdf-parse';
|
|
3
|
+
import { getData } from 'pdf-parse/worker';
|
|
4
|
+
|
|
5
|
+
export const runtime = 'nodejs';
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/doc-editor/parse
|
|
10
|
+
*
|
|
11
|
+
* Accepts a file upload (FormData), parses it, creates an editor session,
|
|
12
|
+
* and returns the parsed document structure.
|
|
13
|
+
*/
|
|
14
|
+
export async function POST(req: Request) {
|
|
15
|
+
try {
|
|
16
|
+
const formData = await req.formData();
|
|
17
|
+
const file = formData.get('file') as File | null;
|
|
18
|
+
|
|
19
|
+
if (!file) {
|
|
20
|
+
return Response.json({ error: 'No file provided' }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
24
|
+
const session = await createSession(buffer, file.name, file.type);
|
|
25
|
+
|
|
26
|
+
// For PDFs, enrich with full text extraction via pdf-parse
|
|
27
|
+
// Skip heavy text extraction for long PDFs (e.g. > 5 pages) to just "view" them quickly
|
|
28
|
+
if (session.type === 'pdf' && session.parsed.rawText === '') {
|
|
29
|
+
if (session.parsed.totalPages <= 5) {
|
|
30
|
+
try {
|
|
31
|
+
// Keep the worker inside the standalone bundle instead of relying on
|
|
32
|
+
// a source-tree path that does not exist in production containers.
|
|
33
|
+
PDFParse.setWorker(getData());
|
|
34
|
+
const parser = new PDFParse({ data: buffer });
|
|
35
|
+
let text = '';
|
|
36
|
+
try {
|
|
37
|
+
text = (await parser.getText()).text;
|
|
38
|
+
} finally {
|
|
39
|
+
await parser.destroy();
|
|
40
|
+
}
|
|
41
|
+
const enrichedParsed = enrichPDFWithText(session.parsed, text);
|
|
42
|
+
session.parsed = enrichedParsed;
|
|
43
|
+
} catch {
|
|
44
|
+
// pdf-parse failed — continue with whatever we have
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
session.parsed.rawText = `[Long PDF - Text extraction skipped for performance. You can still view it.]`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Return a lightweight response (no base64 file data)
|
|
52
|
+
return Response.json({
|
|
53
|
+
sessionId: session.id,
|
|
54
|
+
fileName: session.fileName,
|
|
55
|
+
type: session.type,
|
|
56
|
+
mimeType: session.mimeType,
|
|
57
|
+
fileSize: session.parsed.fileSize,
|
|
58
|
+
totalPages: session.parsed.totalPages,
|
|
59
|
+
fields: session.parsed.fields,
|
|
60
|
+
pages: session.parsed.pages.map((p) => ({
|
|
61
|
+
index: p.index,
|
|
62
|
+
text: p.text.slice(0, 2000), // Limit text for response size
|
|
63
|
+
html: p.html,
|
|
64
|
+
fields: p.fields,
|
|
65
|
+
dimensions: p.dimensions,
|
|
66
|
+
})),
|
|
67
|
+
rawText: session.parsed.rawText.slice(0, 5000),
|
|
68
|
+
metadata: session.parsed.metadata,
|
|
69
|
+
});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : 'Failed to parse file';
|
|
72
|
+
return Response.json({ error: message }, { status: 500 });
|
|
73
|
+
}
|
|
74
|
+
}
|