lytx 0.3.0 → 0.3.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/.env.example +1 -0
- package/README.md +3 -0
- package/alchemy.run.ts +1 -0
- package/cli/setup.ts +1 -0
- package/index.d.ts +1 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/api/ai_api.ts +81 -10
- package/src/config/createLytxAppConfig.ts +15 -1
- package/src/worker.tsx +7 -1
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -72,10 +72,12 @@ export default app satisfies ExportedHandler<Env>;
|
|
|
72
72
|
- `trackingRoutePrefix` (prefix all tracking routes, e.g. `/collect`)
|
|
73
73
|
- `tagRoutes.scriptPath` + `tagRoutes.eventPath` (custom v2 route paths)
|
|
74
74
|
- `auth.emailPasswordEnabled`, `auth.requireEmailVerification`, `auth.socialProviders.google`, `auth.socialProviders.github`
|
|
75
|
+
- `ai.provider`, `ai.baseURL`, `ai.model`, `ai.apiKey` (runtime AI vendor/model overrides)
|
|
75
76
|
- `features.reportBuilderEnabled` + `features.askAiEnabled`
|
|
76
77
|
- `names.*` (typed resource binding names for D1/KV/Queue/DO)
|
|
77
78
|
- `domains.app` + `domains.tracking` (typed host/domain values)
|
|
78
79
|
- `startupValidation.*` + `env.*` (startup env requirement checks with field-level errors)
|
|
80
|
+
- `env.AI_PROVIDER`, `env.AI_BASE_URL`, `env.AI_MODEL` (AI vendor/model routing overrides)
|
|
79
81
|
- `env.EMAIL_FROM` (optional factory override for outgoing email sender)
|
|
80
82
|
|
|
81
83
|
For deployment scripts, use `resolveLytxResourceNames(...)` from `lytx/resource-names` to derive deterministic Cloudflare resource names with optional stage-based prefix/suffix strategy.
|
|
@@ -367,6 +369,7 @@ RESEND_API_KEY=...
|
|
|
367
369
|
|
|
368
370
|
# AI features (optional)
|
|
369
371
|
AI_API_KEY=...
|
|
372
|
+
AI_PROVIDER=openai
|
|
370
373
|
AI_BASE_URL=...
|
|
371
374
|
AI_MODEL=...
|
|
372
375
|
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/index.d.ts
CHANGED
package/index.ts
CHANGED
|
@@ -73,6 +73,7 @@ export type {
|
|
|
73
73
|
LytxDbAdapter,
|
|
74
74
|
LytxEventStore,
|
|
75
75
|
LytxDbConfig,
|
|
76
|
+
LytxAiConfig,
|
|
76
77
|
} from "./src/config/createLytxAppConfig";
|
|
77
78
|
export { DEFAULT_LYTX_RESOURCE_NAMES, resolveLytxResourceNames } from "./src/config/resourceNames";
|
|
78
79
|
export type { LytxResourceNames, LytxResourceNamingOptions, LytxResourceStagePosition } from "./src/config/resourceNames";
|
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
|
|
|
@@ -57,6 +57,17 @@ const domainSchema = z
|
|
|
57
57
|
|
|
58
58
|
const envKeySchema = z.string().trim().min(1, "Env var value cannot be empty");
|
|
59
59
|
|
|
60
|
+
const aiRuntimeConfigSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
provider: envKeySchema.optional(),
|
|
63
|
+
baseURL: z.string().trim().url("ai.baseURL must be a valid URL").optional(),
|
|
64
|
+
model: envKeySchema.optional(),
|
|
65
|
+
apiKey: envKeySchema.optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict();
|
|
68
|
+
|
|
69
|
+
export type LytxAiConfig = z.input<typeof aiRuntimeConfigSchema>;
|
|
70
|
+
|
|
60
71
|
const createLytxAppConfigSchema = z
|
|
61
72
|
.object({
|
|
62
73
|
enableRequestLogging: z.boolean().optional(),
|
|
@@ -65,6 +76,7 @@ const createLytxAppConfigSchema = z
|
|
|
65
76
|
useQueueIngestion: z.boolean().optional(),
|
|
66
77
|
includeLegacyTagRoutes: z.boolean().optional(),
|
|
67
78
|
trackingRoutePrefix: routePrefixSchema.optional(),
|
|
79
|
+
ai: aiRuntimeConfigSchema.optional(),
|
|
68
80
|
auth: z
|
|
69
81
|
.object({
|
|
70
82
|
emailPasswordEnabled: z.boolean().optional(),
|
|
@@ -124,6 +136,8 @@ const createLytxAppConfigSchema = z
|
|
|
124
136
|
BETTER_AUTH_URL: z.string().trim().url("BETTER_AUTH_URL must be a valid URL").optional(),
|
|
125
137
|
ENCRYPTION_KEY: envKeySchema.optional(),
|
|
126
138
|
AI_API_KEY: envKeySchema.optional(),
|
|
139
|
+
AI_BASE_URL: z.string().trim().url("AI_BASE_URL must be a valid URL").optional(),
|
|
140
|
+
AI_PROVIDER: envKeySchema.optional(),
|
|
127
141
|
AI_MODEL: envKeySchema.optional(),
|
|
128
142
|
LYTX_DOMAIN: domainSchema.optional(),
|
|
129
143
|
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.ai?.apiKey ?? parsed_config.env?.AI_API_KEY,
|
|
169
|
+
model: parsed_config.ai?.model ?? parsed_config.env?.AI_MODEL,
|
|
170
|
+
baseURL: parsed_config.ai?.baseURL ?? parsed_config.env?.AI_BASE_URL,
|
|
171
|
+
provider: parsed_config.ai?.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) {
|