From e20e69c4f0d4b010fa3b7dd3ebdeb742694bf4c4 Mon Sep 17 00:00:00 2001 From: nicksolarsoul Date: Tue, 19 May 2026 15:14:56 +0700 Subject: [PATCH 1/3] feat: add optional Hey Jude gateway routing --- README.md | 7 +++++++ backend/.env.example | 5 +++++ backend/src/lib/llm/claude.ts | 7 +++++++ backend/src/lib/llm/gemini.ts | 7 +++++++ backend/src/lib/llm/heyJude.ts | 14 ++++++++++++++ backend/src/lib/userSettings.ts | 12 +++++++++--- 6 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 backend/src/lib/llm/heyJude.ts diff --git a/README.md b/README.md index 9e70f9af6..bd80f5afb 100644 --- a/README.md +++ b/README.md @@ -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 Claude/Gemini 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`: @@ -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 Claude or Gemini prompts before provider 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: diff --git a/backend/.env.example b/backend/.env.example index 6b4d56150..2b594e743 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 Claude/Gemini calls through Hey Jude. +HEY_JUDE_ENABLED=false +HEY_JUDE_BASE_URL=http://localhost:4005 +HEY_JUDE_API_KEY=sk-heyjude-dev diff --git a/backend/src/lib/llm/claude.ts b/backend/src/lib/llm/claude.ts index 9f86b1679..5d22dd583 100644 --- a/backend/src/lib/llm/claude.ts +++ b/backend/src/lib/llm/claude.ts @@ -6,6 +6,7 @@ import type { NormalizedToolCall, NormalizedToolResult, } from "./types"; +import { heyJudeApiKey, heyJudeBaseUrl, heyJudeEnabled } from "./heyJude"; import { toClaudeTools } from "./tools"; type ContentBlock = @@ -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 }); } diff --git a/backend/src/lib/llm/gemini.ts b/backend/src/lib/llm/gemini.ts index e40fc6031..4bc08ec72 100644 --- a/backend/src/lib/llm/gemini.ts +++ b/backend/src/lib/llm/gemini.ts @@ -4,6 +4,7 @@ import type { StreamChatResult, NormalizedToolCall, } from "./types"; +import { heyJudeApiKey, heyJudeBaseUrl, heyJudeEnabled } from "./heyJude"; import { toGeminiTools } from "./tools"; type GeminiPart = { @@ -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) }); } diff --git a/backend/src/lib/llm/heyJude.ts b/backend/src/lib/llm/heyJude.ts new file mode 100644 index 000000000..5c6e56ebe --- /dev/null +++ b/backend/src/lib/llm/heyJude.ts @@ -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"; +} diff --git a/backend/src/lib/userSettings.ts b/backend/src/lib/userSettings.ts index bfbeb0fd5..7215ac3ae 100644 --- a/backend/src/lib/userSettings.ts +++ b/backend/src/lib/userSettings.ts @@ -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; } From b4b205935e36f85ddc373591a9509d413d26e509 Mon Sep 17 00:00:00 2001 From: nicksolarsoul Date: Tue, 19 May 2026 16:22:45 +0700 Subject: [PATCH 2/3] feat: route OpenAI through Hey Jude --- README.md | 4 ++-- backend/.env.example | 2 +- backend/src/lib/llm/openai.ts | 13 ++++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bd80f5afb..ad225f686 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ OPENAI_API_KEY=your-openai-key RESEND_API_KEY=your-resend-key USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret -# Optional: route Claude/Gemini calls through Hey Jude. +# 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 @@ -81,7 +81,7 @@ 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 Claude or Gemini prompts before provider 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. +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 diff --git a/backend/.env.example b/backend/.env.example index 2b594e743..c01f9d070 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -19,7 +19,7 @@ OPENAI_API_KEY=your-openai-key RESEND_API_KEY=your-resend-key USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret -# Optional: route Claude/Gemini calls through Hey Jude. +# 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 diff --git a/backend/src/lib/llm/openai.ts b/backend/src/lib/llm/openai.ts index de07b5c96..e38f33b7a 100644 --- a/backend/src/lib/llm/openai.ts +++ b/backend/src/lib/llm/openai.ts @@ -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; @@ -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( @@ -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", @@ -115,7 +126,7 @@ async function createResponse(params: { reasoningSummary?: boolean; apiKey: string; }): Promise { - const response = await fetch(OPENAI_RESPONSES_URL, { + const response = await fetch(responsesUrl(), { method: "POST", headers: { Authorization: `Bearer ${params.apiKey}`, From 28c8fab5b26cbc76a1d2eab512a4822bd152b9be Mon Sep 17 00:00:00 2001 From: nicksolarsoul Date: Mon, 25 May 2026 19:55:48 +0700 Subject: [PATCH 3/3] fix: include chat message workflow column --- backend/schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/schema.sql b/backend/schema.sql index b6a4e934a..506d4ac35 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -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() );