Repository navigation
Expand file tree
/
Copy pathindex.ts
More file actions
110 lines (109 loc) · 7.43 KB
/
Copy pathindex.ts
File metadata and controls
110 lines (109 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import {createEnv} from "@t3-oss/env-core";
import {z} from "zod";
export const env = createEnv({
server: {
SUPABASE_URL: z.string().url(),
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
INTELLIGENCE_PROVIDER: z.enum(["ollama", "gateway", "anthropic", "jev"]).default("ollama"),
// Provider for features that generate text (reply suggestions, knowledge drafts). Defaults to INTELLIGENCE_PROVIDER, or ollama when that is jev.
TEXT_PROVIDER: z.enum(["ollama", "gateway", "anthropic"]).optional(),
AI_GATEWAY_API_KEY: z.string().min(1).optional(),
AI_GATEWAY_PROVIDER_ROUTES: z.string().min(1).optional(),
CLASSIFICATION_MODE: z.enum(['language', 'decision']).optional(),
AI_GATEWAY_CLASSIFICATION_MODEL: z.string().min(1).optional(),
AI_GATEWAY_DECISION_MODEL: z.string().min(1).optional(),
AI_GATEWAY_REPLY_MODEL: z.string().min(1).optional(),
AI_GATEWAY_KNOWLEDGE_DECISION_MODEL: z.string().min(1).optional(),
AI_GATEWAY_KNOWLEDGE_DRAFT_MODEL: z.string().min(1).optional(),
AI_GATEWAY_EVIDENCE_ANALYSIS_MODEL: z.string().min(1).optional(),
AI_GATEWAY_MODEL: z.string().min(1).optional(),
AI_GATEWAY_ASSISTANT_MODEL: z.string().min(1).optional(),
AI_GATEWAY_TEXT_MODEL: z.string().min(1).optional(),
AI_GATEWAY_EMBEDDING_MODEL: z.string().min(1).optional(),
EMBEDDING_PROVIDER: z.enum(["ollama", "gateway", "voyage"]).default("ollama"),
ANTHROPIC_MODEL: z.string().min(1).optional(),
JEV_MODEL: z.string().min(1).optional(),
OLLAMA_BASE_URL: z.string().url().optional(),
OLLAMA_MODEL: z.string().optional().default("granite4.1:3b"),
OLLAMA_EMBEDDING_MODEL: z.string().optional().default("nomic-embed-text-v2-moe"),
OLLAMA_EMBEDDING_DIMENSIONS: z.coerce.number().int().positive().optional(),
// Dimensions of the configured embedding model. Must equal the vector columns in the database (768); the API refuses to start otherwise.
EMBEDDING_DIMENSIONS: z.coerce.number().int().positive().optional(),
// Minimum cosine similarity for knowledge retrieval and for selecting tickets affected by approved knowledge.
EMBEDDING_SIMILARITY_THRESHOLD: z.coerce.number().min(0.7).max(0.9).default(0.8),
// Most tickets queued for evaluation per approved knowledge version.
KNOWLEDGE_CANDIDATE_LIMIT: z.coerce.number().int().min(1).max(200).default(25),
FAST_PATH_SCAN_SIZE: z.coerce.number().int().positive().default(30),
// Minimum relevant classifications before the wizard "Continue" button enables.
// Does NOT cap how many emails Tier 1 classifies — that's FAST_PATH_SCAN_SIZE.
FAST_PATH_CONTINUE_THRESHOLD: z.coerce.number().int().positive().default(3),
FAST_PATH_CONCURRENCY: z.coerce.number().int().positive().default(3),
// Caps concurrent classifyEmailWithMeta calls WITHIN a single tier1/
// tier2/tier3 run — distinct from FAST_PATH_CONCURRENCY, which caps
// concurrent *runs* of the whole Inngest function (e.g. two accounts
// onboarding at once). Without this, a single run fires every scanned
// message's classification at once via Promise.allSettled, with no cap
// at all. Anthropic's
// per-minute rate limits (RPM/ITPM/OTPM) comfortably absorb that fan-out
// (verified: even Sonnet 4.x's lowest "Start" tier is 1,000 RPM / 2M ITPM —
// 30 requests isn't close), so this exists for local Ollama, where the real
// ceiling is OLLAMA_NUM_PARALLEL (Ollama's own internal per-model parallel
// request limit, which defaults to 1-4 depending on available RAM) — exceed
// it and requests queue inside Ollama or time out client-side. Default is
// deliberately conservative for local dev; raise it per-environment once
// measured (see docs/observability.md-style benchmarking, not guessed).
FAST_PATH_LLM_CONCURRENCY: z.coerce.number().int().positive().default(2),
// Consecutive classification failures (after each message's own
// withRetry already exhausted its attempts) within a single tier1/
// tier2/tier3 run before the circuit breaker opens and the rest of
// the run stops attempting new classifications — the backend is down
// or saturated, and burning through the remaining batch against it
// wastes time, concurrency slots, and (on a paid provider) money.
FAST_PATH_CIRCUIT_BREAKER_THRESHOLD: z.coerce.number().int().positive().default(3),
// Total classification attempts a message can accumulate across ALL
// 5 call sites (tier1/tier2/tier3/incremental-sync/gmail-poll) plus
// the classification-retry-sweep cron combined, before it's marked
// classification_status: 'failed_permanent' and given up on. Not the
// same as withRetry's own internal 4 attempts
// (apps/api/src/lib/retry.ts's CLASSIFICATION_RETRY_POLICY) — this
// counts separate pipeline dispatches over time (minutes/hours
// apart), that one counts sub-second retries within a single
// dispatch.
CLASSIFICATION_MAX_ATTEMPTS: z.coerce.number().int().positive().default(5),
BACKGROUND_CONCURRENCY: z.coerce.number().int().positive().default(3),
TIER_2_WINDOW_DAYS: z.coerce.number().int().positive().default(15),
MAX_EMAIL_AGE_DAYS: z.coerce.number().int().positive().default(90),
BATCH_SYNC_LIMIT: z.coerce.number().int().positive().default(20),
INCREMENTAL_SYNC_CONCURRENCY: z.coerce.number().int().positive().default(3),
// Kelan backoffice — optional comma-separated admin email allowlist
KELAN_ADMIN_EMAILS: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().min(1).optional(),
// KAI-126: Langfuse (self-hosted) LLM tracing. Unset → tracing is a no-op.
LANGFUSE_BASE_URL: z.string().url().optional(),
LANGFUSE_PUBLIC_KEY: z.string().min(1).optional(),
LANGFUSE_SECRET_KEY: z.string().min(1).optional(),
// KAI-126: ClickStack (OpenTelemetry) app observability. Unset → OTel SDK disabled.
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
OTEL_SERVICE_NAME: z.string().min(1).default("kairo-api"),
// Inngest event-sending credentials (landing BFF → Inngest cloud).
// Optional: when unset, dispatch is skipped (e.g. local dev without Inngest).
INNGEST_EVENT_KEY: z.string().min(1).optional(),
// KAI-202 kill-switch: disable onboarding pipeline dispatch from OAuth callback
// without redeploying. Any value other than "true" leaves dispatch enabled.
DISABLE_ONBOARDING_PIPELINE_DISPATCH: z.string().optional(),
// KAI-201 feature flag: enable detection UI in onboarding wizard (default: off).
// Set FEATURE_FLAG_ENABLE_DETECTION_UI=true to enable.
FEATURE_FLAG_ENABLE_DETECTION_UI: z.string().optional(),
// KAI-245: platform-wide fallback for {{privacy_url}} in transactional emails
// when accounts.privacy_url is not set per-tenant.
PRIVACY_URL: z.string().url().default("https://kairo.alejojamc.com/privacy/"),
},
clientPrefix: "VITE_",
client: {
VITE_SUPABASE_URL: z.string().url().optional(),
VITE_SUPABASE_ANON_KEY: z.string().min(1).optional(),
},
runtimeEnv: process.env,
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
emptyStringAsUndefined: true,
});