Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ ANTHROPIC_API_KEY=your-anthropic-key
OPENAI_API_KEY=your-openai-key
RESEND_API_KEY=your-resend-key
USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret

# Optional: route model calls through Hey Jude.
HEY_JUDE_ENABLED=false
HEY_JUDE_BASE_URL=http://localhost:4005
HEY_JUDE_API_KEY=sk-heyjude-dev
```

Create `frontend/.env.local`:
Expand All @@ -76,6 +81,8 @@ Supabase values come from the project dashboard. Use the project URL for `SUPABA

Provider keys are only needed for the models and email features you plan to use. Model provider keys can be configured in `backend/.env` for the whole instance, or per user in **Account > Models & API Keys**. If a provider key is present in `backend/.env`, that provider is available by default and the matching browser API key field is read-only.

To pseudonymize provider prompts before model calls, run [Hey Jude](https://github.com/sure-scale/hey-jude) locally and set `HEY_JUDE_ENABLED=true`. Mike still stores original chat text in its database.

## Install

Install each app package:
Expand Down
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ ANTHROPIC_API_KEY=your-anthropic-key
OPENAI_API_KEY=your-openai-key
RESEND_API_KEY=your-resend-key
USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret

# Optional: route model calls through Hey Jude.
HEY_JUDE_ENABLED=false
HEY_JUDE_BASE_URL=http://localhost:4005
HEY_JUDE_API_KEY=sk-heyjude-dev
1 change: 1 addition & 0 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ create table if not exists public.chat_messages (
role text not null,
content jsonb,
files jsonb,
workflow jsonb,
annotations jsonb,
created_at timestamptz not null default now()
);
Expand Down
7 changes: 7 additions & 0 deletions backend/src/lib/llm/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
NormalizedToolCall,
NormalizedToolResult,
} from "./types";
import { heyJudeApiKey, heyJudeBaseUrl, heyJudeEnabled } from "./heyJude";
import { toClaudeTools } from "./tools";

type ContentBlock =
Expand All @@ -31,6 +32,12 @@ function apiKey(override?: string | null): string {
}

function client(override?: string | null): Anthropic {
if (heyJudeEnabled()) {
return new Anthropic({
apiKey: heyJudeApiKey(),
baseURL: heyJudeBaseUrl(),
});
}
const apiKeyValue = apiKey(override);
return new Anthropic({ apiKey: apiKeyValue });
}
Expand Down
7 changes: 7 additions & 0 deletions backend/src/lib/llm/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
StreamChatResult,
NormalizedToolCall,
} from "./types";
import { heyJudeApiKey, heyJudeBaseUrl, heyJudeEnabled } from "./heyJude";
import { toGeminiTools } from "./tools";

type GeminiPart = {
Expand Down Expand Up @@ -39,6 +40,12 @@ function apiKey(override?: string | null): string {
}

function client(override?: string | null): GoogleGenAI {
if (heyJudeEnabled()) {
return new GoogleGenAI({
apiKey: heyJudeApiKey(),
httpOptions: { baseUrl: heyJudeBaseUrl() },
});
}
return new GoogleGenAI({ apiKey: apiKey(override) });
}

Expand Down
14 changes: 14 additions & 0 deletions backend/src/lib/llm/heyJude.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function heyJudeEnabled(): boolean {
return process.env.HEY_JUDE_ENABLED === "true";
}

export function heyJudeBaseUrl(): string {
return (process.env.HEY_JUDE_BASE_URL || "http://localhost:4005").replace(
/\/$/,
"",
);
}

export function heyJudeApiKey(): string {
return process.env.HEY_JUDE_API_KEY || "sk-heyjude-dev";
}
13 changes: 12 additions & 1 deletion backend/src/lib/llm/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
StreamChatParams,
StreamChatResult,
} from "./types";
import { heyJudeApiKey, heyJudeBaseUrl, heyJudeEnabled } from "./heyJude";

const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
const MAX_OUTPUT_TOKENS = 16384;
Expand Down Expand Up @@ -36,6 +37,9 @@ type ResponseStreamEvent = {
};

function apiKey(override?: string | null): string {
if (heyJudeEnabled()) {
return heyJudeApiKey();
}
const key = override?.trim() || process.env.OPENAI_API_KEY?.trim() || "";
if (!key) {
throw new Error(
Expand All @@ -45,6 +49,13 @@ function apiKey(override?: string | null): string {
return key;
}

function responsesUrl(): string {
if (heyJudeEnabled()) {
return `${heyJudeBaseUrl()}/v1/responses`;
}
return OPENAI_RESPONSES_URL;
}

function toResponseTools(tools: OpenAIToolSchema[]): ResponseFunctionTool[] {
return tools.map((tool) => ({
type: "function",
Expand Down Expand Up @@ -115,7 +126,7 @@ async function createResponse(params: {
reasoningSummary?: boolean;
apiKey: string;
}): Promise<Response> {
const response = await fetch(OPENAI_RESPONSES_URL, {
const response = await fetch(responsesUrl(), {
method: "POST",
headers: {
Authorization: `Bearer ${params.apiKey}`,
Expand Down
12 changes: 9 additions & 3 deletions backend/src/lib/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ export type UserModelSettings = {
// available, otherwise OpenAI nano, otherwise Claude Haiku. With no user keys
// set, defaults to Gemini (the dev-mode env fallback).
function resolveTitleModel(apiKeys: UserApiKeys): string {
if (apiKeys.gemini?.trim()) return DEFAULT_TITLE_MODEL;
if (apiKeys.openai?.trim()) return OPENAI_LOW_MODELS[0];
if (apiKeys.claude?.trim()) return "claude-haiku-4-5";
if (apiKeys.gemini?.trim() || process.env.GEMINI_API_KEY?.trim()) {
return DEFAULT_TITLE_MODEL;
}
if (apiKeys.openai?.trim() || process.env.OPENAI_API_KEY?.trim()) {
return OPENAI_LOW_MODELS[0];
}
if (apiKeys.claude?.trim() || process.env.ANTHROPIC_API_KEY?.trim()) {
return "claude-haiku-4-5";
}
return DEFAULT_TITLE_MODEL;
}

Expand Down