@flowselections/ai-authenticatie 1.0.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/dist-lib/_core-safelist.d.ts +2 -0
- package/dist-lib/_core-safelist.d.ts.map +1 -0
- package/dist-lib/_core-safelist.js +15 -0
- package/dist-lib/components/AiAuthenticatieLogo.d.ts +3 -0
- package/dist-lib/components/AiAuthenticatieLogo.d.ts.map +1 -0
- package/dist-lib/components/AiAuthenticatieLogo.js +4 -0
- package/dist-lib/components/AiAuthenticatiePage.d.ts +4 -0
- package/dist-lib/components/AiAuthenticatiePage.d.ts.map +1 -0
- package/dist-lib/components/AiAuthenticatiePage.js +145 -0
- package/dist-lib/components/AiModuleDetailPage.d.ts +2 -0
- package/dist-lib/components/AiModuleDetailPage.d.ts.map +1 -0
- package/dist-lib/components/AiModuleDetailPage.js +43 -0
- package/dist-lib/components/AiModuleTestPanel.d.ts +4 -0
- package/dist-lib/components/AiModuleTestPanel.d.ts.map +1 -0
- package/dist-lib/components/AiModuleTestPanel.js +38 -0
- package/dist-lib/components/AiPeriodFilter.d.ts +7 -0
- package/dist-lib/components/AiPeriodFilter.d.ts.map +1 -0
- package/dist-lib/components/AiPeriodFilter.js +14 -0
- package/dist-lib/components/AiStatTile.d.ts +6 -0
- package/dist-lib/components/AiStatTile.d.ts.map +1 -0
- package/dist-lib/components/AiStatTile.js +5 -0
- package/dist-lib/components/ModuleAgentsDialog.d.ts +14 -0
- package/dist-lib/components/ModuleAgentsDialog.d.ts.map +1 -0
- package/dist-lib/components/ModuleAgentsDialog.js +78 -0
- package/dist-lib/components/ProviderLogo.d.ts +8 -0
- package/dist-lib/components/ProviderLogo.d.ts.map +1 -0
- package/dist-lib/components/ProviderLogo.js +24 -0
- package/dist-lib/components/ProviderManageDialog.d.ts +11 -0
- package/dist-lib/components/ProviderManageDialog.d.ts.map +1 -0
- package/dist-lib/components/ProviderManageDialog.js +173 -0
- package/dist-lib/components/settings/AiAuthenticatieSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/AiAuthenticatieSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/AiAuthenticatieSettingsCard.js +7 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
- package/dist-lib/hooks/useAdminCredentials.d.ts +14 -0
- package/dist-lib/hooks/useAdminCredentials.d.ts.map +1 -0
- package/dist-lib/hooks/useAdminCredentials.js +17 -0
- package/dist-lib/hooks/useAiAdmin.d.ts +44 -0
- package/dist-lib/hooks/useAiAdmin.d.ts.map +1 -0
- package/dist-lib/hooks/useAiAdmin.js +32 -0
- package/dist-lib/hooks/useAiCostOverview.d.ts +7 -0
- package/dist-lib/hooks/useAiCostOverview.d.ts.map +1 -0
- package/dist-lib/hooks/useAiCostOverview.js +28 -0
- package/dist-lib/hooks/useAiForGoal.d.ts +21 -0
- package/dist-lib/hooks/useAiForGoal.d.ts.map +1 -0
- package/dist-lib/hooks/useAiForGoal.js +40 -0
- package/dist-lib/hooks/useAiForModule.d.ts +11 -0
- package/dist-lib/hooks/useAiForModule.d.ts.map +1 -0
- package/dist-lib/hooks/useAiForModule.js +11 -0
- package/dist-lib/hooks/useAiGoals.d.ts +8 -0
- package/dist-lib/hooks/useAiGoals.d.ts.map +1 -0
- package/dist-lib/hooks/useAiGoals.js +22 -0
- package/dist-lib/hooks/useAiModules.d.ts +15 -0
- package/dist-lib/hooks/useAiModules.d.ts.map +1 -0
- package/dist-lib/hooks/useAiModules.js +59 -0
- package/dist-lib/hooks/useAiPricing.d.ts +20 -0
- package/dist-lib/hooks/useAiPricing.d.ts.map +1 -0
- package/dist-lib/hooks/useAiPricing.js +97 -0
- package/dist-lib/hooks/useAiUsage.d.ts +12 -0
- package/dist-lib/hooks/useAiUsage.d.ts.map +1 -0
- package/dist-lib/hooks/useAiUsage.js +90 -0
- package/dist-lib/hooks/useAiUsageByModule.d.ts +8 -0
- package/dist-lib/hooks/useAiUsageByModule.d.ts.map +1 -0
- package/dist-lib/hooks/useAiUsageByModule.js +22 -0
- package/dist-lib/hooks/useIsAiAdmin.d.ts +6 -0
- package/dist-lib/hooks/useIsAiAdmin.d.ts.map +1 -0
- package/dist-lib/hooks/useIsAiAdmin.js +5 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
- package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
- package/dist-lib/hooks/useMijnModuleData.js +49 -0
- package/dist-lib/hooks/useModules.d.ts +14 -0
- package/dist-lib/hooks/useModules.d.ts.map +1 -0
- package/dist-lib/hooks/useModules.js +47 -0
- package/dist-lib/index.d.ts +29 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +33 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
- package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +15971 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.js +49 -0
- package/dist-lib/integrations/supabase/client.d.ts +15967 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +13 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +15967 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.server.js +30 -0
- package/dist-lib/integrations/supabase/types.d.ts +16477 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/types.js +78 -0
- package/dist-lib/lib/aiChat.functions.d.ts +31948 -0
- package/dist-lib/lib/aiChat.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiChat.functions.js +51 -0
- package/dist-lib/lib/aiChat.server.d.ts +8 -0
- package/dist-lib/lib/aiChat.server.d.ts.map +1 -0
- package/dist-lib/lib/aiChat.server.js +347 -0
- package/dist-lib/lib/aiChat.stream.d.ts +33 -0
- package/dist-lib/lib/aiChat.stream.d.ts.map +1 -0
- package/dist-lib/lib/aiChat.stream.js +77 -0
- package/dist-lib/lib/aiChat.stream.server.d.ts +38 -0
- package/dist-lib/lib/aiChat.stream.server.d.ts.map +1 -0
- package/dist-lib/lib/aiChat.stream.server.js +259 -0
- package/dist-lib/lib/aiChat.types.d.ts +63 -0
- package/dist-lib/lib/aiChat.types.d.ts.map +1 -0
- package/dist-lib/lib/aiChat.types.js +1 -0
- package/dist-lib/lib/aiCredentialTest.functions.d.ts +31959 -0
- package/dist-lib/lib/aiCredentialTest.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiCredentialTest.functions.js +714 -0
- package/dist-lib/lib/aiGoals.functions.d.ts +12912 -0
- package/dist-lib/lib/aiGoals.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiGoals.functions.js +14 -0
- package/dist-lib/lib/aiModules.functions.d.ts +95851 -0
- package/dist-lib/lib/aiModules.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiModules.functions.js +41 -0
- package/dist-lib/lib/aiModules.server.d.ts +65 -0
- package/dist-lib/lib/aiModules.server.d.ts.map +1 -0
- package/dist-lib/lib/aiModules.server.js +119 -0
- package/dist-lib/lib/aiPeriod.d.ts +11 -0
- package/dist-lib/lib/aiPeriod.d.ts.map +1 -0
- package/dist-lib/lib/aiPeriod.js +53 -0
- package/dist-lib/lib/aiPricing.functions.d.ts +31943 -0
- package/dist-lib/lib/aiPricing.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiPricing.functions.js +13 -0
- package/dist-lib/lib/aiPricing.server.d.ts +19 -0
- package/dist-lib/lib/aiPricing.server.d.ts.map +1 -0
- package/dist-lib/lib/aiPricing.server.js +97 -0
- package/dist-lib/lib/aiUsage.functions.d.ts +5 -0
- package/dist-lib/lib/aiUsage.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiUsage.functions.js +12 -0
- package/dist-lib/lib/aiUsage.server.d.ts +3 -0
- package/dist-lib/lib/aiUsage.server.d.ts.map +1 -0
- package/dist-lib/lib/aiUsage.server.js +229 -0
- package/dist-lib/lib/aiUsage.types.d.ts +21 -0
- package/dist-lib/lib/aiUsage.types.d.ts.map +1 -0
- package/dist-lib/lib/aiUsage.types.js +1 -0
- package/dist-lib/lib/aiUsageByModule.functions.d.ts +31952 -0
- package/dist-lib/lib/aiUsageByModule.functions.d.ts.map +1 -0
- package/dist-lib/lib/aiUsageByModule.functions.js +37 -0
- package/dist-lib/lib/modules.functions.d.ts +47924 -0
- package/dist-lib/lib/modules.functions.d.ts.map +1 -0
- package/dist-lib/lib/modules.functions.js +21 -0
- package/dist-lib/lib/modules.server.d.ts +23 -0
- package/dist-lib/lib/modules.server.d.ts.map +1 -0
- package/dist-lib/lib/modules.server.js +52 -0
- package/dist-lib/lib/utils.d.ts +3 -0
- package/dist-lib/lib/utils.d.ts.map +1 -0
- package/dist-lib/lib/utils.js +5 -0
- package/dist-lib/lib/validationSchemas.d.ts +15 -0
- package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
- package/dist-lib/lib/validationSchemas.js +25 -0
- package/dist-lib/styles.css +1 -0
- package/package.json +92 -0
- package/public/flowselections-assets/ai-authenticatie/claude.png +0 -0
- package/public/flowselections-assets/ai-authenticatie/gemini.png +0 -0
- package/public/flowselections-assets/ai-authenticatie/openai.png +0 -0
- package/public/logo.png +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { supabaseAdmin } from '../integrations/supabase/client.server';
|
|
2
|
+
function isChatError(v) {
|
|
3
|
+
return !!v && typeof v === 'object' && v.ok === false;
|
|
4
|
+
}
|
|
5
|
+
const TIMEOUT_MS = 60000;
|
|
6
|
+
async function logCall(params) {
|
|
7
|
+
const { result } = params;
|
|
8
|
+
try {
|
|
9
|
+
await supabaseAdmin.rpc('log_ai_call', {
|
|
10
|
+
_consumer_module_id: params.consumerModuleId,
|
|
11
|
+
_provider_slug: params.providerSlug,
|
|
12
|
+
_model_slug: params.modelSlug,
|
|
13
|
+
_status: result.ok ? 'ok' : result.code,
|
|
14
|
+
_input_tokens: result.ok ? result.usage.inputTokens : 0,
|
|
15
|
+
_output_tokens: result.ok ? result.usage.outputTokens : 0,
|
|
16
|
+
_total_tokens: result.ok ? result.usage.totalTokens : 0,
|
|
17
|
+
_cost_usd: result.ok ? result.costUsd : null,
|
|
18
|
+
_duration_ms: params.durationMs,
|
|
19
|
+
_error_code: result.ok ? null : result.code,
|
|
20
|
+
_error_message: result.ok ? null : result.message,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// logging never blokkeert de call
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const OPENAI_DEFAULT = 'https://api.openai.com/v1';
|
|
28
|
+
const ANTHROPIC_DEFAULT = 'https://api.anthropic.com';
|
|
29
|
+
const GEMINI_DEFAULT = 'https://generativelanguage.googleapis.com/v1beta';
|
|
30
|
+
function err(status, code, message) {
|
|
31
|
+
return { ok: false, status, code, message };
|
|
32
|
+
}
|
|
33
|
+
function withTimeout() {
|
|
34
|
+
const ctrl = new AbortController();
|
|
35
|
+
const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
36
|
+
return { signal: ctrl.signal, done: () => clearTimeout(t) };
|
|
37
|
+
}
|
|
38
|
+
function classifyHttp(status, providerName, body) {
|
|
39
|
+
const snippet = body.slice(0, 300);
|
|
40
|
+
if (status === 401)
|
|
41
|
+
return err(401, 'unauthorized', `${providerName}: API-sleutel is ongeldig of ingetrokken.`);
|
|
42
|
+
if (status === 403)
|
|
43
|
+
return err(403, 'forbidden', `${providerName}: geen toegang tot dit model of deze functie.`);
|
|
44
|
+
if (status === 429) {
|
|
45
|
+
if (/quota|billing|insufficient/i.test(body)) {
|
|
46
|
+
return err(429, 'quota_exceeded', `${providerName}: creditlimiet bereikt of factureringsprobleem.`);
|
|
47
|
+
}
|
|
48
|
+
return err(429, 'rate_limited', `${providerName}: te veel aanvragen. Probeer het over enkele momenten opnieuw.`);
|
|
49
|
+
}
|
|
50
|
+
if (status === 400)
|
|
51
|
+
return err(400, 'bad_request', `${providerName}: aanvraag geweigerd. ${snippet}`);
|
|
52
|
+
return err(status, 'provider_error', `${providerName} gaf een fout (${status}). ${snippet}`);
|
|
53
|
+
}
|
|
54
|
+
function trimBase(url) {
|
|
55
|
+
return url.trim().replace(/\/+$/, '');
|
|
56
|
+
}
|
|
57
|
+
// ─── Provider adapters ────────────────────────────────────────────────
|
|
58
|
+
async function callOpenAI(apiKey, baseUrl, modelSlug, messages, options) {
|
|
59
|
+
const base = trimBase(baseUrl || OPENAI_DEFAULT);
|
|
60
|
+
const url = `${base}/chat/completions`;
|
|
61
|
+
const { signal, done } = withTimeout();
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
signal,
|
|
66
|
+
headers: {
|
|
67
|
+
'content-type': 'application/json',
|
|
68
|
+
authorization: `Bearer ${apiKey}`,
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
model: modelSlug,
|
|
72
|
+
messages,
|
|
73
|
+
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
74
|
+
...(options.maxTokens != null ? { max_tokens: options.maxTokens } : {}),
|
|
75
|
+
...(options.topP != null ? { top_p: options.topP } : {}),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
return classifyHttp(res.status, 'OpenAI', await res.text());
|
|
80
|
+
const json = (await res.json());
|
|
81
|
+
const content = json.choices?.[0]?.message?.content ?? '';
|
|
82
|
+
const u = json.usage ?? {};
|
|
83
|
+
return {
|
|
84
|
+
content,
|
|
85
|
+
finishReason: json.choices?.[0]?.finish_reason ?? null,
|
|
86
|
+
usage: {
|
|
87
|
+
inputTokens: u.prompt_tokens ?? 0,
|
|
88
|
+
outputTokens: u.completion_tokens ?? 0,
|
|
89
|
+
totalTokens: u.total_tokens ?? (u.prompt_tokens ?? 0) + (u.completion_tokens ?? 0),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
const msg = e instanceof Error && e.name === 'AbortError' ? 'Timeout: OpenAI reageerde niet op tijd.' : e.message;
|
|
95
|
+
return err(0, 'network', msg);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
done();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function callAnthropic(apiKey, baseUrl, modelSlug, messages, options) {
|
|
102
|
+
const base = trimBase(baseUrl || ANTHROPIC_DEFAULT);
|
|
103
|
+
const url = `${base}/v1/messages`;
|
|
104
|
+
// Anthropic vereist system als top-level string en messages zonder system-rol.
|
|
105
|
+
const systemParts = messages.filter(m => m.role === 'system').map(m => m.content).filter(Boolean);
|
|
106
|
+
const rest = messages
|
|
107
|
+
.filter(m => m.role !== 'system')
|
|
108
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
109
|
+
const { signal, done } = withTimeout();
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(url, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
signal,
|
|
114
|
+
headers: {
|
|
115
|
+
'content-type': 'application/json',
|
|
116
|
+
'x-api-key': apiKey,
|
|
117
|
+
'anthropic-version': '2023-06-01',
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
model: modelSlug,
|
|
121
|
+
max_tokens: options.maxTokens ?? 1024,
|
|
122
|
+
...(systemParts.length ? { system: systemParts.join('\n\n') } : {}),
|
|
123
|
+
messages: rest,
|
|
124
|
+
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
125
|
+
...(options.topP != null ? { top_p: options.topP } : {}),
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok)
|
|
129
|
+
return classifyHttp(res.status, 'Anthropic', await res.text());
|
|
130
|
+
const json = (await res.json());
|
|
131
|
+
const content = (json.content ?? [])
|
|
132
|
+
.filter(p => p.type === 'text' && typeof p.text === 'string')
|
|
133
|
+
.map(p => p.text)
|
|
134
|
+
.join('');
|
|
135
|
+
const u = json.usage ?? {};
|
|
136
|
+
return {
|
|
137
|
+
content,
|
|
138
|
+
finishReason: json.stop_reason ?? null,
|
|
139
|
+
usage: {
|
|
140
|
+
inputTokens: u.input_tokens ?? 0,
|
|
141
|
+
outputTokens: u.output_tokens ?? 0,
|
|
142
|
+
totalTokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
const msg = e instanceof Error && e.name === 'AbortError' ? 'Timeout: Anthropic reageerde niet op tijd.' : e.message;
|
|
148
|
+
return err(0, 'network', msg);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
done();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function normalizeGeminiBase(baseUrl) {
|
|
155
|
+
const raw = trimBase(baseUrl || GEMINI_DEFAULT);
|
|
156
|
+
try {
|
|
157
|
+
const u = new URL(raw);
|
|
158
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
159
|
+
const versionIdx = parts.findIndex(p => /^v\d+(alpha|beta)?$/i.test(p));
|
|
160
|
+
const version = versionIdx >= 0 ? parts[versionIdx] : 'v1beta';
|
|
161
|
+
u.pathname = `/${version}`;
|
|
162
|
+
u.search = '';
|
|
163
|
+
u.hash = '';
|
|
164
|
+
return trimBase(u.toString());
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return GEMINI_DEFAULT;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function callGemini(apiKey, baseUrl, modelSlug, messages, options) {
|
|
171
|
+
const base = normalizeGeminiBase(baseUrl);
|
|
172
|
+
const modelPath = modelSlug.startsWith('models/') ? modelSlug : `models/${modelSlug}`;
|
|
173
|
+
const url = `${base}/${modelPath}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
174
|
+
const systemParts = messages.filter(m => m.role === 'system').map(m => m.content).filter(Boolean);
|
|
175
|
+
const contents = messages
|
|
176
|
+
.filter(m => m.role !== 'system')
|
|
177
|
+
.map(m => ({
|
|
178
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
179
|
+
parts: [{ text: m.content }],
|
|
180
|
+
}));
|
|
181
|
+
const body = {
|
|
182
|
+
contents,
|
|
183
|
+
generationConfig: {
|
|
184
|
+
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
185
|
+
...(options.maxTokens != null ? { maxOutputTokens: options.maxTokens } : {}),
|
|
186
|
+
...(options.topP != null ? { topP: options.topP } : {}),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
if (systemParts.length) {
|
|
190
|
+
body.systemInstruction = { role: 'system', parts: [{ text: systemParts.join('\n\n') }] };
|
|
191
|
+
}
|
|
192
|
+
const { signal, done } = withTimeout();
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(url, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
signal,
|
|
197
|
+
headers: { 'content-type': 'application/json' },
|
|
198
|
+
body: JSON.stringify(body),
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok)
|
|
201
|
+
return classifyHttp(res.status, 'Google Gemini', await res.text());
|
|
202
|
+
const json = (await res.json());
|
|
203
|
+
const content = (json.candidates?.[0]?.content?.parts ?? [])
|
|
204
|
+
.map(p => p.text ?? '').join('');
|
|
205
|
+
const u = json.usageMetadata ?? {};
|
|
206
|
+
return {
|
|
207
|
+
content,
|
|
208
|
+
finishReason: json.candidates?.[0]?.finishReason ?? null,
|
|
209
|
+
usage: {
|
|
210
|
+
inputTokens: u.promptTokenCount ?? 0,
|
|
211
|
+
outputTokens: u.candidatesTokenCount ?? 0,
|
|
212
|
+
totalTokens: u.totalTokenCount ?? (u.promptTokenCount ?? 0) + (u.candidatesTokenCount ?? 0),
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
const msg = e instanceof Error && e.name === 'AbortError' ? 'Timeout: Google Gemini reageerde niet op tijd.' : e.message;
|
|
218
|
+
return err(0, 'network', msg);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
done();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function moduleAiEnabled(consumerModuleId) {
|
|
225
|
+
const { data } = await supabaseAdmin
|
|
226
|
+
.from('ai_modules')
|
|
227
|
+
.select('module_key, ai_enabled')
|
|
228
|
+
.eq('module_key', consumerModuleId)
|
|
229
|
+
.maybeSingle();
|
|
230
|
+
return !!data && data.ai_enabled === true;
|
|
231
|
+
}
|
|
232
|
+
async function resolveBinding(consumerModuleId) {
|
|
233
|
+
if (!(await moduleAiEnabled(consumerModuleId))) {
|
|
234
|
+
return err(404, 'no_binding', `Module "${consumerModuleId}" heeft geen AI ingeschakeld in AI-instellingen.`);
|
|
235
|
+
}
|
|
236
|
+
const { data: bindings, error: bErr } = await supabaseAdmin
|
|
237
|
+
.from('ai_module_bindings')
|
|
238
|
+
.select('id, model_id, credential_id, is_default')
|
|
239
|
+
.eq('consumer_module_id', consumerModuleId)
|
|
240
|
+
.order('is_default', { ascending: false });
|
|
241
|
+
if (bErr)
|
|
242
|
+
return err(500, 'provider_error', `Binding ophalen mislukt: ${bErr.message}`);
|
|
243
|
+
const binding = Array.isArray(bindings) ? bindings[0] : null;
|
|
244
|
+
if (!binding)
|
|
245
|
+
return err(404, 'no_binding', `Geen AI-agent gekoppeld aan module "${consumerModuleId}".`);
|
|
246
|
+
const [{ data: model, error: mErr }, { data: cred, error: cErr }] = await Promise.all([
|
|
247
|
+
supabaseAdmin.from('ai_models').select('id, slug, display_name, provider_id, active').eq('id', binding.model_id).maybeSingle(),
|
|
248
|
+
supabaseAdmin.from('ai_credentials').select('id, api_key, active, provider_id').eq('id', binding.credential_id).maybeSingle(),
|
|
249
|
+
]);
|
|
250
|
+
if (mErr || !model)
|
|
251
|
+
return err(404, 'not_configured', 'Gekoppeld model niet gevonden.');
|
|
252
|
+
if (cErr || !cred)
|
|
253
|
+
return err(404, 'not_configured', 'Gekoppelde API-sleutel niet gevonden.');
|
|
254
|
+
if (!cred.active)
|
|
255
|
+
return err(403, 'not_configured', 'De gekoppelde API-sleutel is uitgeschakeld.');
|
|
256
|
+
if (model.active === false)
|
|
257
|
+
return err(403, 'not_configured', 'Het gekoppelde model is uitgeschakeld.');
|
|
258
|
+
const { data: provider, error: pErr } = await supabaseAdmin
|
|
259
|
+
.from('ai_providers').select('id, slug, name, base_url, active').eq('id', model.provider_id).maybeSingle();
|
|
260
|
+
if (pErr || !provider)
|
|
261
|
+
return err(404, 'not_configured', 'Provider niet gevonden.');
|
|
262
|
+
if (provider.active === false)
|
|
263
|
+
return err(403, 'not_configured', 'De provider is uitgeschakeld.');
|
|
264
|
+
return {
|
|
265
|
+
provider: { id: provider.id, slug: provider.slug, name: provider.name, base_url: provider.base_url },
|
|
266
|
+
model: { id: model.id, slug: model.slug, display_name: model.display_name },
|
|
267
|
+
credential: { id: cred.id, api_key: cred.api_key, active: cred.active },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async function lookupPricing(providerSlug, modelSlug) {
|
|
271
|
+
const { data } = await supabaseAdmin
|
|
272
|
+
.from('ai_model_pricing')
|
|
273
|
+
.select('input_per_mtok, output_per_mtok')
|
|
274
|
+
.eq('provider_slug', providerSlug)
|
|
275
|
+
.eq('model_slug', modelSlug)
|
|
276
|
+
.maybeSingle();
|
|
277
|
+
if (!data)
|
|
278
|
+
return null;
|
|
279
|
+
return { input: data.input_per_mtok, output: data.output_per_mtok };
|
|
280
|
+
}
|
|
281
|
+
function computeCost(usage, pricing) {
|
|
282
|
+
if (!pricing)
|
|
283
|
+
return null;
|
|
284
|
+
const inCost = pricing.input != null ? (usage.inputTokens / 1000000) * pricing.input : 0;
|
|
285
|
+
const outCost = pricing.output != null ? (usage.outputTokens / 1000000) * pricing.output : 0;
|
|
286
|
+
if (pricing.input == null && pricing.output == null)
|
|
287
|
+
return null;
|
|
288
|
+
return inCost + outCost;
|
|
289
|
+
}
|
|
290
|
+
// ─── Public server-only entrypoints ───────────────────────────────────
|
|
291
|
+
export async function runAiChatForRequest(input) {
|
|
292
|
+
const started = Date.now();
|
|
293
|
+
const resolved = await resolveBinding(input.consumerModuleId);
|
|
294
|
+
if (isChatError(resolved)) {
|
|
295
|
+
await logCall({
|
|
296
|
+
consumerModuleId: input.consumerModuleId,
|
|
297
|
+
providerSlug: null, modelSlug: null,
|
|
298
|
+
result: resolved, durationMs: Date.now() - started,
|
|
299
|
+
});
|
|
300
|
+
return resolved;
|
|
301
|
+
}
|
|
302
|
+
const { provider, model, credential } = resolved;
|
|
303
|
+
const slug = provider.slug.toLowerCase();
|
|
304
|
+
const options = input.options ?? {};
|
|
305
|
+
let providerRes;
|
|
306
|
+
if (slug.includes('openai') || slug.includes('gpt')) {
|
|
307
|
+
providerRes = await callOpenAI(credential.api_key, provider.base_url, model.slug, input.messages, options);
|
|
308
|
+
}
|
|
309
|
+
else if (slug.includes('anthropic') || slug.includes('claude')) {
|
|
310
|
+
providerRes = await callAnthropic(credential.api_key, provider.base_url, model.slug, input.messages, options);
|
|
311
|
+
}
|
|
312
|
+
else if (slug.includes('gemini') || slug.includes('google')) {
|
|
313
|
+
providerRes = await callGemini(credential.api_key, provider.base_url, model.slug, input.messages, options);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const r = err(400, 'unsupported_provider', `Provider "${provider.name}" wordt niet ondersteund voor chat.`);
|
|
317
|
+
await logCall({ consumerModuleId: input.consumerModuleId, providerSlug: provider.slug, modelSlug: model.slug, result: r, durationMs: Date.now() - started });
|
|
318
|
+
return r;
|
|
319
|
+
}
|
|
320
|
+
if (isChatError(providerRes)) {
|
|
321
|
+
await logCall({ consumerModuleId: input.consumerModuleId, providerSlug: provider.slug, modelSlug: model.slug, result: providerRes, durationMs: Date.now() - started });
|
|
322
|
+
return providerRes;
|
|
323
|
+
}
|
|
324
|
+
const pricing = await lookupPricing(provider.slug, model.slug);
|
|
325
|
+
const result = {
|
|
326
|
+
ok: true,
|
|
327
|
+
content: providerRes.content,
|
|
328
|
+
provider: { slug: provider.slug, name: provider.name },
|
|
329
|
+
model: { slug: model.slug, displayName: model.display_name },
|
|
330
|
+
usage: providerRes.usage,
|
|
331
|
+
finishReason: providerRes.finishReason,
|
|
332
|
+
costUsd: computeCost(providerRes.usage, pricing),
|
|
333
|
+
};
|
|
334
|
+
await logCall({ consumerModuleId: input.consumerModuleId, providerSlug: provider.slug, modelSlug: model.slug, result, durationMs: Date.now() - started });
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
export async function describeBindingForRequest(consumerModuleId) {
|
|
338
|
+
const resolved = await resolveBinding(consumerModuleId);
|
|
339
|
+
if (isChatError(resolved)) {
|
|
340
|
+
return { ready: false, provider: null, model: null, reason: resolved.message };
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
ready: true,
|
|
344
|
+
provider: { slug: resolved.provider.slug, name: resolved.provider.name },
|
|
345
|
+
model: { slug: resolved.model.slug, displayName: resolved.model.display_name },
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AiChatError, AiChatMessage, AiChatOptions, AiChatUsage } from './aiChat.types';
|
|
2
|
+
export type AiStreamHandlers = {
|
|
3
|
+
onMeta?: (meta: {
|
|
4
|
+
provider: {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
model: {
|
|
9
|
+
slug: string;
|
|
10
|
+
displayName: string;
|
|
11
|
+
};
|
|
12
|
+
}) => void;
|
|
13
|
+
onToken?: (delta: string) => void;
|
|
14
|
+
onUsage?: (usage: AiChatUsage, costUsd: number | null) => void;
|
|
15
|
+
onError?: (err: Pick<AiChatError, 'code' | 'message' | 'status'>) => void;
|
|
16
|
+
onDone?: () => void;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Consumer-API: stream tokens vanuit het gekoppelde AI-model voor een doel.
|
|
21
|
+
* Retourneert de volledige tekst na afronding.
|
|
22
|
+
*/
|
|
23
|
+
export declare function streamAiChat(input: {
|
|
24
|
+
consumerModuleId: string;
|
|
25
|
+
messages: AiChatMessage[];
|
|
26
|
+
options?: AiChatOptions;
|
|
27
|
+
} & AiStreamHandlers): Promise<{
|
|
28
|
+
ok: boolean;
|
|
29
|
+
text: string;
|
|
30
|
+
usage?: AiChatUsage;
|
|
31
|
+
costUsd?: number | null;
|
|
32
|
+
}>;
|
|
33
|
+
//# sourceMappingURL=aiChat.stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiChat.stream.d.ts","sourceRoot":"","sources":["../../src/lib/aiChat.stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IACpH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC,KAAK,IAAI,CAAC;IAC1E,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE;IACxC,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,GAAG,gBAAgB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAmD1G"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { supabase } from '@flowselections/core';
|
|
2
|
+
/**
|
|
3
|
+
* Consumer-API: stream tokens vanuit het gekoppelde AI-model voor een doel.
|
|
4
|
+
* Retourneert de volledige tekst na afronding.
|
|
5
|
+
*/
|
|
6
|
+
export async function streamAiChat(input) {
|
|
7
|
+
const { data: sess } = await supabase.auth.getSession();
|
|
8
|
+
const token = sess?.session?.access_token;
|
|
9
|
+
if (!token) {
|
|
10
|
+
input.onError?.({ code: 'unauthorized', message: 'Geen actieve sessie.', status: 401 });
|
|
11
|
+
return { ok: false, text: '' };
|
|
12
|
+
}
|
|
13
|
+
const res = await fetch('/api/ai/stream', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
consumerModuleId: input.consumerModuleId,
|
|
18
|
+
messages: input.messages,
|
|
19
|
+
options: input.options,
|
|
20
|
+
}),
|
|
21
|
+
signal: input.signal,
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok || !res.body) {
|
|
24
|
+
input.onError?.({ code: 'network', message: `HTTP ${res.status}`, status: res.status });
|
|
25
|
+
return { ok: false, text: '' };
|
|
26
|
+
}
|
|
27
|
+
const reader = res.body.getReader();
|
|
28
|
+
const decoder = new TextDecoder();
|
|
29
|
+
let buffer = '';
|
|
30
|
+
let text = '';
|
|
31
|
+
let usage;
|
|
32
|
+
let costUsd;
|
|
33
|
+
let hadError = false;
|
|
34
|
+
outer: while (true) {
|
|
35
|
+
const { value, done } = await reader.read();
|
|
36
|
+
if (done)
|
|
37
|
+
break;
|
|
38
|
+
buffer += decoder.decode(value, { stream: true });
|
|
39
|
+
const parts = buffer.split(/\r?\n\r?\n/);
|
|
40
|
+
buffer = parts.pop() ?? '';
|
|
41
|
+
for (const chunk of parts) {
|
|
42
|
+
const lines = chunk.split(/\r?\n/);
|
|
43
|
+
let dataLine = '';
|
|
44
|
+
for (const l of lines)
|
|
45
|
+
if (l.startsWith('data:'))
|
|
46
|
+
dataLine += l.slice(5).trimStart();
|
|
47
|
+
if (!dataLine)
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const ev = JSON.parse(dataLine);
|
|
51
|
+
switch (ev.type) {
|
|
52
|
+
case 'meta':
|
|
53
|
+
input.onMeta?.({ provider: ev.provider, model: ev.model });
|
|
54
|
+
break;
|
|
55
|
+
case 'token':
|
|
56
|
+
text += ev.delta;
|
|
57
|
+
input.onToken?.(ev.delta);
|
|
58
|
+
break;
|
|
59
|
+
case 'usage':
|
|
60
|
+
usage = ev.usage;
|
|
61
|
+
costUsd = ev.costUsd;
|
|
62
|
+
input.onUsage?.(ev.usage, ev.costUsd);
|
|
63
|
+
break;
|
|
64
|
+
case 'error':
|
|
65
|
+
hadError = true;
|
|
66
|
+
input.onError?.({ code: ev.code, message: ev.message, status: ev.status });
|
|
67
|
+
break;
|
|
68
|
+
case 'done':
|
|
69
|
+
input.onDone?.();
|
|
70
|
+
break outer;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch { /* skip */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { ok: !hadError, text, usage, costUsd };
|
|
77
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AiChatError, AiChatMessage, AiChatOptions, AiChatUsage } from './aiChat.types';
|
|
2
|
+
/**
|
|
3
|
+
* Server-side streaming: verbind met provider, geef via generator delta-tokens en usage.
|
|
4
|
+
* Wordt aangeroepen door /api/ai/stream server-route.
|
|
5
|
+
*/
|
|
6
|
+
type StreamEvent = {
|
|
7
|
+
type: 'meta';
|
|
8
|
+
provider: {
|
|
9
|
+
slug: string;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
model: {
|
|
13
|
+
slug: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
};
|
|
16
|
+
} | {
|
|
17
|
+
type: 'token';
|
|
18
|
+
delta: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'usage';
|
|
21
|
+
usage: AiChatUsage;
|
|
22
|
+
costUsd: number | null;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'error';
|
|
25
|
+
code: AiChatError['code'];
|
|
26
|
+
message: string;
|
|
27
|
+
status: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'done';
|
|
30
|
+
};
|
|
31
|
+
export declare function streamAiChatForRequest(input: {
|
|
32
|
+
consumerModuleId: string;
|
|
33
|
+
messages: AiChatMessage[];
|
|
34
|
+
options?: AiChatOptions;
|
|
35
|
+
bearerUserId: string | null;
|
|
36
|
+
}): AsyncGenerator<StreamEvent>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=aiChat.stream.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiChat.stream.server.d.ts","sourceRoot":"","sources":["../../src/lib/aiChat.stream.server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7F;;;GAGG;AAEH,KAAK,WAAW,GACZ;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACxG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,wBAAuB,sBAAsB,CAAC,KAAK,EAAE;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG,cAAc,CAAC,WAAW,CAAC,CA2F9B"}
|