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 CHANGED
@@ -24,6 +24,7 @@ RESEND_API_KEY=
24
24
 
25
25
  # AI features (optional)
26
26
  AI_API_KEY=
27
+ AI_PROVIDER=openai
27
28
  AI_BASE_URL=
28
29
  AI_MODEL=
29
30
  AI_DAILY_TOKEN_LIMIT=
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
@@ -350,6 +350,7 @@ EMAIL_FROM=noreply@yourdomain.com
350
350
 
351
351
  # AI features (optional)
352
352
  AI_API_KEY=
353
+ AI_PROVIDER=openai
353
354
  AI_BASE_URL=
354
355
  AI_MODEL=
355
356
  AI_DAILY_TOKEN_LIMIT=
package/index.d.ts CHANGED
@@ -51,6 +51,7 @@ export type {
51
51
  LytxDbAdapter,
52
52
  LytxEventStore,
53
53
  LytxDbConfig,
54
+ LytxAiConfig,
54
55
  } from "./src/config/createLytxAppConfig";
55
56
  export function createLytxApp(
56
57
  config?: CreateLytxAppConfig,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lytx",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {
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 baseURL = env.AI_BASE_URL?.trim() || DEFAULT_AI_BASE_URL;
52
- const model = env.AI_MODEL?.trim() || DEFAULT_AI_MODEL;
53
- const apiKey = env.AI_API_KEY?.trim() ?? "";
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.baseURL,
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.baseURL,
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.baseURL,
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.baseURL,
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.baseURL,
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.baseURL,
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/kit/blob/master/core/docs/oss-contract.md#supported-extension-and-customization-points";
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) {