lytx 0.3.0 → 0.3.1
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/.env.example +1 -0
- package/README.md +2 -0
- package/alchemy.run.ts +1 -0
- package/cli/setup.ts +1 -0
- package/package.json +1 -1
- package/src/api/ai_api.ts +81 -10
- package/src/config/createLytxAppConfig.ts +3 -1
- package/src/worker.tsx +7 -1
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ export default app satisfies ExportedHandler<Env>;
|
|
|
76
76
|
- `names.*` (typed resource binding names for D1/KV/Queue/DO)
|
|
77
77
|
- `domains.app` + `domains.tracking` (typed host/domain values)
|
|
78
78
|
- `startupValidation.*` + `env.*` (startup env requirement checks with field-level errors)
|
|
79
|
+
- `env.AI_PROVIDER`, `env.AI_BASE_URL`, `env.AI_MODEL` (AI vendor/model routing overrides)
|
|
79
80
|
- `env.EMAIL_FROM` (optional factory override for outgoing email sender)
|
|
80
81
|
|
|
81
82
|
For deployment scripts, use `resolveLytxResourceNames(...)` from `lytx/resource-names` to derive deterministic Cloudflare resource names with optional stage-based prefix/suffix strategy.
|
|
@@ -367,6 +368,7 @@ RESEND_API_KEY=...
|
|
|
367
368
|
|
|
368
369
|
# AI features (optional)
|
|
369
370
|
AI_API_KEY=...
|
|
371
|
+
AI_PROVIDER=openai
|
|
370
372
|
AI_BASE_URL=...
|
|
371
373
|
AI_MODEL=...
|
|
372
374
|
AI_DAILY_TOKEN_LIMIT=
|
package/alchemy.run.ts
CHANGED
|
@@ -139,6 +139,7 @@ export const worker = await Redwood(resourceNames.workerName, {
|
|
|
139
139
|
RESEND_API_KEY: alchemy.secret(process.env.RESEND_API_KEY),
|
|
140
140
|
ENCRYPTION_KEY: alchemy.secret(process.env.ENCRYPTION_KEY),
|
|
141
141
|
AI_API_KEY: alchemy.secret(process.env.AI_API_KEY),
|
|
142
|
+
AI_PROVIDER: process.env.AI_PROVIDER ?? "openai",
|
|
142
143
|
SEED_DATA_SECRET: alchemy.secret(process.env.SEED_DATA_SECRET),
|
|
143
144
|
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
|
|
144
145
|
GITHUB_CLIENT_ID: alchemy.secret(process.env.GITHUB_CLIENT_ID),
|
package/cli/setup.ts
CHANGED
package/package.json
CHANGED
package/src/api/ai_api.ts
CHANGED
|
@@ -22,17 +22,71 @@ import {
|
|
|
22
22
|
import { parseDateParam, parseSiteIdParam } from "@/utilities/dashboardParams";
|
|
23
23
|
|
|
24
24
|
type AiConfig = {
|
|
25
|
+
provider: string;
|
|
25
26
|
baseURL: string;
|
|
26
27
|
model: string;
|
|
27
28
|
apiKey: string;
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
type AiRuntimeOverrides = {
|
|
32
|
+
provider?: string;
|
|
33
|
+
baseURL?: string;
|
|
34
|
+
model?: string;
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
30
38
|
type NivoChartType = "bar" | "line" | "pie";
|
|
31
39
|
|
|
32
40
|
const DEFAULT_AI_MODEL = "gpt-5-mini";
|
|
41
|
+
const DEFAULT_AI_PROVIDER = "openai";
|
|
33
42
|
const DEFAULT_AI_BASE_URL = "https://api.openai.com/v1";
|
|
34
43
|
const MAX_METRIC_LIMIT = 100;
|
|
35
44
|
|
|
45
|
+
const AI_PROVIDER_BASE_URLS: Record<string, string> = {
|
|
46
|
+
openai: "https://api.openai.com/v1",
|
|
47
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
48
|
+
groq: "https://api.groq.com/openai/v1",
|
|
49
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
50
|
+
xai: "https://api.x.ai/v1",
|
|
51
|
+
ollama: "http://localhost:11434/v1",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const AI_PROVIDER_DEFAULT_MODELS: Record<string, string> = {
|
|
55
|
+
openai: "gpt-5-mini",
|
|
56
|
+
openrouter: "openai/gpt-4o-mini",
|
|
57
|
+
groq: "llama-3.1-70b-versatile",
|
|
58
|
+
deepseek: "deepseek-chat",
|
|
59
|
+
xai: "grok-2-latest",
|
|
60
|
+
ollama: "llama3.2",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let aiRuntimeOverrides: AiRuntimeOverrides = {};
|
|
64
|
+
|
|
65
|
+
function normalizeOptionalString(value: unknown): string | null {
|
|
66
|
+
if (typeof value !== "string") return null;
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveAiBaseUrl(provider: string, explicitBaseUrl: string | null): string {
|
|
72
|
+
if (explicitBaseUrl) return explicitBaseUrl;
|
|
73
|
+
return AI_PROVIDER_BASE_URLS[provider] ?? DEFAULT_AI_BASE_URL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveAiModel(provider: string, explicitModel: string | null): string {
|
|
77
|
+
if (explicitModel) return explicitModel;
|
|
78
|
+
return AI_PROVIDER_DEFAULT_MODELS[provider] ?? DEFAULT_AI_MODEL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function setAiRuntimeOverrides(overrides: AiRuntimeOverrides = {}): void {
|
|
82
|
+
aiRuntimeOverrides = {
|
|
83
|
+
provider: normalizeOptionalString(overrides.provider) ?? undefined,
|
|
84
|
+
baseURL: normalizeOptionalString(overrides.baseURL) ?? undefined,
|
|
85
|
+
model: normalizeOptionalString(overrides.model) ?? undefined,
|
|
86
|
+
apiKey: normalizeOptionalString(overrides.apiKey) ?? undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
36
90
|
type TokenUsage = {
|
|
37
91
|
inputTokens: number | null;
|
|
38
92
|
outputTokens: number | null;
|
|
@@ -48,13 +102,28 @@ type StreamCompletionSummary = {
|
|
|
48
102
|
};
|
|
49
103
|
|
|
50
104
|
function getAiConfigFromEnv(): AiConfig | null {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
105
|
+
const envValues = env as unknown as Record<string, unknown>;
|
|
106
|
+
const providerRaw =
|
|
107
|
+
normalizeOptionalString(aiRuntimeOverrides.provider)
|
|
108
|
+
?? normalizeOptionalString(envValues.AI_PROVIDER)
|
|
109
|
+
?? DEFAULT_AI_PROVIDER;
|
|
110
|
+
const provider = providerRaw.toLowerCase();
|
|
111
|
+
const baseURL = resolveAiBaseUrl(
|
|
112
|
+
provider,
|
|
113
|
+
normalizeOptionalString(aiRuntimeOverrides.baseURL) ?? normalizeOptionalString(envValues.AI_BASE_URL),
|
|
114
|
+
);
|
|
115
|
+
const model = resolveAiModel(
|
|
116
|
+
provider,
|
|
117
|
+
normalizeOptionalString(aiRuntimeOverrides.model) ?? normalizeOptionalString(envValues.AI_MODEL),
|
|
118
|
+
);
|
|
119
|
+
const apiKey =
|
|
120
|
+
normalizeOptionalString(aiRuntimeOverrides.apiKey)
|
|
121
|
+
?? normalizeOptionalString(envValues.AI_API_KEY)
|
|
122
|
+
?? "";
|
|
54
123
|
|
|
55
124
|
if (!apiKey) return null;
|
|
56
125
|
|
|
57
|
-
return { baseURL, model, apiKey };
|
|
126
|
+
return { provider, baseURL, model, apiKey };
|
|
58
127
|
}
|
|
59
128
|
|
|
60
129
|
function clampLimit(value: unknown, fallback = 10): number {
|
|
@@ -353,6 +422,8 @@ export const aiConfigRoute = route(
|
|
|
353
422
|
return new Response(
|
|
354
423
|
JSON.stringify({
|
|
355
424
|
configured: Boolean(config),
|
|
425
|
+
provider: config?.provider ?? "",
|
|
426
|
+
baseURL: config?.baseURL ?? "",
|
|
356
427
|
model: config?.model ?? "",
|
|
357
428
|
}),
|
|
358
429
|
{
|
|
@@ -567,7 +638,7 @@ export const aiTagSuggestRoute = route(
|
|
|
567
638
|
site_id: site.site_id,
|
|
568
639
|
request_id: requestId,
|
|
569
640
|
request_type: "site_tag_suggest",
|
|
570
|
-
provider: aiConfig.
|
|
641
|
+
provider: aiConfig.provider,
|
|
571
642
|
model: aiConfig.model,
|
|
572
643
|
status: "error",
|
|
573
644
|
error_code: "daily_limit_exceeded",
|
|
@@ -610,7 +681,7 @@ export const aiTagSuggestRoute = route(
|
|
|
610
681
|
site_id: site.site_id,
|
|
611
682
|
request_id: requestId,
|
|
612
683
|
request_type: "site_tag_suggest",
|
|
613
|
-
provider: aiConfig.
|
|
684
|
+
provider: aiConfig.provider,
|
|
614
685
|
model: aiConfig.model,
|
|
615
686
|
status: "success",
|
|
616
687
|
input_tokens: usage.inputTokens,
|
|
@@ -647,7 +718,7 @@ export const aiTagSuggestRoute = route(
|
|
|
647
718
|
site_id: site.site_id,
|
|
648
719
|
request_id: requestId,
|
|
649
720
|
request_type: "site_tag_suggest",
|
|
650
|
-
provider: aiConfig.
|
|
721
|
+
provider: aiConfig.provider,
|
|
651
722
|
model: aiConfig.model,
|
|
652
723
|
status: "error",
|
|
653
724
|
error_code: "ai_request_failed",
|
|
@@ -736,7 +807,7 @@ export const aiChatRoute = route(
|
|
|
736
807
|
site_id: defaultToolSiteId,
|
|
737
808
|
request_id: requestId,
|
|
738
809
|
request_type: "chat",
|
|
739
|
-
provider: aiConfig.
|
|
810
|
+
provider: aiConfig.provider,
|
|
740
811
|
model: aiConfig.model,
|
|
741
812
|
status: "error",
|
|
742
813
|
error_code: "daily_limit_exceeded",
|
|
@@ -1105,7 +1176,7 @@ export const aiChatRoute = route(
|
|
|
1105
1176
|
site_id: defaultToolSiteId,
|
|
1106
1177
|
request_id: requestId,
|
|
1107
1178
|
request_type: "chat",
|
|
1108
|
-
provider: aiConfig.
|
|
1179
|
+
provider: aiConfig.provider,
|
|
1109
1180
|
model: aiConfig.model,
|
|
1110
1181
|
status: streamSummary.finishReason === "error" ? "error" : "success",
|
|
1111
1182
|
error_code: streamSummary.finishReason === "error" ? "stream_finish_error" : null,
|
|
@@ -1134,7 +1205,7 @@ export const aiChatRoute = route(
|
|
|
1134
1205
|
site_id: defaultToolSiteId,
|
|
1135
1206
|
request_id: requestId,
|
|
1136
1207
|
request_type: "chat",
|
|
1137
|
-
provider: aiConfig.
|
|
1208
|
+
provider: aiConfig.provider,
|
|
1138
1209
|
model: aiConfig.model,
|
|
1139
1210
|
status: "error",
|
|
1140
1211
|
error_code: "chat_request_failed",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export const CREATE_LYTX_APP_CONFIG_DOC_URL =
|
|
4
|
-
"https://github.com/lytx-io/
|
|
4
|
+
"https://github.com/lytx-io/lytx/blob/master/core/docs/oss-contract.md#supported-extension-and-customization-points";
|
|
5
5
|
|
|
6
6
|
const dbAdapterValues = ["sqlite", "postgres", "singlestore", "analytics_engine"] as const;
|
|
7
7
|
|
|
@@ -124,6 +124,8 @@ const createLytxAppConfigSchema = z
|
|
|
124
124
|
BETTER_AUTH_URL: z.string().trim().url("BETTER_AUTH_URL must be a valid URL").optional(),
|
|
125
125
|
ENCRYPTION_KEY: envKeySchema.optional(),
|
|
126
126
|
AI_API_KEY: envKeySchema.optional(),
|
|
127
|
+
AI_BASE_URL: z.string().trim().url("AI_BASE_URL must be a valid URL").optional(),
|
|
128
|
+
AI_PROVIDER: envKeySchema.optional(),
|
|
127
129
|
AI_MODEL: envKeySchema.optional(),
|
|
128
130
|
LYTX_DOMAIN: domainSchema.optional(),
|
|
129
131
|
EMAIL_FROM: z.string().trim().email("EMAIL_FROM must be a valid email address").optional(),
|
package/src/worker.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { eventsApi } from "@api/events_api";
|
|
|
9
9
|
import { seedApi } from "@api/seed_api";
|
|
10
10
|
import { team_dashboard_endpoints } from "@api/team_api";
|
|
11
11
|
import { world_countries, getCurrentVisitorsRoute, getDashboardDataCore, getDashboardDataRoute, siteEventsSqlRoute, siteEventsSchemaRoute } from "@api/sites_api";
|
|
12
|
-
import { aiChatRoute, aiConfigRoute, aiTagSuggestRoute, getAiConfig } from "@api/ai_api";
|
|
12
|
+
import { aiChatRoute, aiConfigRoute, aiTagSuggestRoute, getAiConfig, setAiRuntimeOverrides } from "@api/ai_api";
|
|
13
13
|
import { resendVerificationEmailRoute, userApiRoutes } from "@api/auth_api";
|
|
14
14
|
import { eventLabelsApi } from "@api/event_labels_api";
|
|
15
15
|
import { reportsApi } from "@api/reports_api";
|
|
@@ -164,6 +164,12 @@ export function createLytxApp(config: CreateLytxAppConfig = {}) {
|
|
|
164
164
|
const parsed_config = parseCreateLytxAppConfig(config);
|
|
165
165
|
setAuthRuntimeConfig(parsed_config.auth);
|
|
166
166
|
setEmailFromAddress(parsed_config.env?.EMAIL_FROM);
|
|
167
|
+
setAiRuntimeOverrides({
|
|
168
|
+
apiKey: parsed_config.env?.AI_API_KEY,
|
|
169
|
+
model: parsed_config.env?.AI_MODEL,
|
|
170
|
+
baseURL: parsed_config.env?.AI_BASE_URL,
|
|
171
|
+
provider: parsed_config.env?.AI_PROVIDER,
|
|
172
|
+
});
|
|
167
173
|
const authProviders = getAuthProviderAvailability();
|
|
168
174
|
const emailPasswordEnabled = parsed_config.auth?.emailPasswordEnabled ?? true;
|
|
169
175
|
if (!emailPasswordEnabled && !authProviders.google && !authProviders.github) {
|