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
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ ENV CLICKHOUSE_HOST=http://localhost:8123
ENV MINIO_ROOT_USER=minioadmin
ENV MINIO_ROOT_PASSWORD=minioadmin

# Default environment variables for supervisord (can be overridden at runtime with -e)
# These are read by supervisord.conf using %(ENV_VAR)s syntax
# Default environment variables (can be overridden at runtime via docker-compose or -e)
# These are inherited by all supervisord child processes automatically
ENV NEXT_PUBLIC_HELICONE_JAWN_SERVICE=http://localhost:8585
ENV S3_ENDPOINT=http://localhost:9080
ENV S3_ACCESS_KEY=minioadmin
Expand All @@ -161,6 +161,11 @@ ENV S3_BUCKET_NAME=request-response-storage
ENV S3_PROMPT_BUCKET_NAME=prompt-body-storage
ENV BETTER_AUTH_SECRET=change-me-in-production

# Entrypoint generates __ENV.js from NEXT_PUBLIC_* env vars before starting services
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

# --------------------------------------------------------------------------------------------------------------------
Expand Down
31 changes: 31 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
set -e

# Generate __ENV.js from all NEXT_PUBLIC_* environment variables
# This ensures the frontend has correct runtime values from docker-compose
# before Next.js starts (defense-in-depth alongside next-runtime-env)

ENV_JS_PATH="/app/web/public/__ENV.js"

# Build JSON object from all NEXT_PUBLIC_* env vars
JSON="{"
FIRST=true
while IFS='=' read -r KEY VALUE; do
if [[ "$KEY" == NEXT_PUBLIC_* ]]; then
if [ "$FIRST" = true ]; then
FIRST=false
else
JSON+=","
fi
# Escape backslashes and double quotes in the value
ESCAPED_VALUE=$(printf '%s' "$VALUE" | sed 's/\\/\\\\/g; s/"/\\"/g')
JSON+="\"$KEY\":\"$ESCAPED_VALUE\""
fi
done < <(env | sort)
JSON+="}"

echo "window.__ENV = $JSON;" > "$ENV_JS_PATH"
echo "Generated $ENV_JS_PATH with NEXT_PUBLIC_* environment variables"

# Execute the original command (supervisord)
exec "$@"
10 changes: 6 additions & 4 deletions supervisord.conf
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ stdout_logfile=/var/log/supervisor/jawn.out.log
stdout_logfile_maxbytes=50MB
stderr_logfile_maxbytes=50MB
user=root
; S3_ENDPOINT and BETTER_AUTH_SECRET can be overridden via Docker env vars for remote deployments
environment=S3_ACCESS_KEY="%(ENV_S3_ACCESS_KEY)s",S3_SECRET_KEY="%(ENV_S3_SECRET_KEY)s",S3_ENDPOINT="%(ENV_S3_ENDPOINT)s",S3_PROMPT_BUCKET_NAME="%(ENV_S3_PROMPT_BUCKET_NAME)s",S3_BUCKET_NAME="%(ENV_S3_BUCKET_NAME)s",KAFKA_CREDS='{"KAFKA_ENABLED": "false", "UPSTASH_KAFKA_BROKER": "localhost:9092", "UPSTASH_KAFKA_URL": "http://localhost:9092", "LOCAL_KAFKA": true}',OPENROUTER_WORKER_URL="http://localhost:8788",OPENAI_API_KEY="sk-...",OPENROUTER_API_KEY="sk-....",PROVIDER_KEYS='{ "DEMO_OPENAI_API_KEY": "sk-..." }',CSB_API_KEY="sk-...",TOGETHER_API_KEY="sk-...",STRIPE_SECRET_KEY="sk_dummy_key_12345678901234567890123456789012345678901234567890",NEXT_PUBLIC_BETTER_AUTH="true",BETTER_AUTH_SECRET="%(ENV_BETTER_AUTH_SECRET)s",DATABASE_URL="http://localhost:8123",SUPABASE_DATABASE_URL="postgresql://postgres:password@localhost:5432/helicone_test",HELICONE_WORKER_URL="http://localhost:8585/v1/gateway/oai",NODE_ENV="development",LOG_LEVEL="debug"
; S3_*, BETTER_AUTH_SECRET, and NEXT_PUBLIC_* vars are inherited from Docker env (set in Dockerfile/docker-compose)
; Do NOT re-declare them here with %(ENV_...)s — that can override docker-compose values with Dockerfile defaults
environment=KAFKA_CREDS='{"KAFKA_ENABLED": "false", "UPSTASH_KAFKA_BROKER": "localhost:9092", "UPSTASH_KAFKA_URL": "http://localhost:9092", "LOCAL_KAFKA": true}',OPENROUTER_WORKER_URL="http://localhost:8788",OPENAI_API_KEY="sk-...",OPENROUTER_API_KEY="sk-....",PROVIDER_KEYS='{ "DEMO_OPENAI_API_KEY": "sk-..." }',CSB_API_KEY="sk-...",TOGETHER_API_KEY="sk-...",STRIPE_SECRET_KEY="sk_dummy_key_12345678901234567890123456789012345678901234567890",NEXT_PUBLIC_BETTER_AUTH="true",DATABASE_URL="http://localhost:8123",SUPABASE_DATABASE_URL="postgresql://postgres:password@localhost:5432/helicone_test",HELICONE_WORKER_URL="http://localhost:8585/v1/gateway/oai",NODE_ENV="development",LOG_LEVEL="debug"

[program:web]
command=yarn start
Expand All @@ -51,8 +52,9 @@ autorestart=true
stderr_logfile=/var/log/supervisor/web.err.log
stdout_logfile=/var/log/supervisor/web.out.log
user=root
; NEXT_PUBLIC_HELICONE_JAWN_SERVICE must be set to public URL for remote deployments (browser connects to this)
environment=VERCEL="1",VERCEL_ENV="development",NEXT_PUBLIC_SLACK_CLIENT_ID="1234567890",SLACK_CLIENT_SECRET="1234567890",NEXT_PUBLIC_BASE_PATH="https://oai.helicone.ai/v1",NEXT_PUBLIC_HELICONE_JAWN_SERVICE="%(ENV_NEXT_PUBLIC_HELICONE_JAWN_SERVICE)s",DATABASE_URL="postgresql://postgres:password@localhost:5432/helicone_test",BETTER_AUTH_SECRET="%(ENV_BETTER_AUTH_SECRET)s",NEXT_PUBLIC_BETTER_AUTH="true"
; NEXT_PUBLIC_HELICONE_JAWN_SERVICE and BETTER_AUTH_SECRET are inherited from Docker env (set in Dockerfile/docker-compose)
; Do NOT re-declare them here with %(ENV_...)s — that can override docker-compose values with Dockerfile defaults
environment=VERCEL="1",VERCEL_ENV="development",NEXT_PUBLIC_SLACK_CLIENT_ID="1234567890",SLACK_CLIENT_SECRET="1234567890",NEXT_PUBLIC_BASE_PATH="https://oai.helicone.ai/v1",DATABASE_URL="postgresql://postgres:password@localhost:5432/helicone_test",NEXT_PUBLIC_BETTER_AUTH="true"

[program:flyway-migrate]
command=/bin/bash -c "until pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 2; done && flyway migrate"
Expand Down
39 changes: 30 additions & 9 deletions worker/src/lib/ai-gateway/AttemptBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export class AttemptBuilder {
orgId: string,
bodyMapping: BodyMappingType = "OPENAI",
plugins?: Plugin[],
globalIgnoreProviders?: Set<ModelProviderName>
globalIgnoreProviders?: Set<ModelProviderName>,
inlineApiKey?: string
): Promise<Attempt[]> {
const allAttempts: Attempt[] = [];

Expand All @@ -61,7 +62,8 @@ export class AttemptBuilder {
modelSpec.data,
orgId,
bodyMapping,
plugins
plugins,
inlineApiKey
);
// Sort this model's attempts (BYOK first), but preserve order relative to other models
allAttempts.push(...sortAttemptsByPriority(providerAttempts));
Expand All @@ -72,7 +74,8 @@ export class AttemptBuilder {
orgId,
bodyMapping,
plugins,
globalIgnoreProviders
globalIgnoreProviders,
inlineApiKey
);
allAttempts.push(...sortAttemptsByPriority(attempts));
}
Expand All @@ -93,7 +96,8 @@ export class AttemptBuilder {
orgId: string,
bodyMapping: BodyMappingType = "OPENAI",
plugins?: Plugin[],
globalIgnoreProviders?: Set<ModelProviderName>
globalIgnoreProviders?: Set<ModelProviderName>,
inlineApiKey?: string
): Promise<Attempt[]> {
// Get all provider data in one query
const providerDataResult = registry.getModelProviderEntriesByModel(
Expand Down Expand Up @@ -129,7 +133,8 @@ export class AttemptBuilder {
data,
orgId,
bodyMapping,
plugins
plugins,
inlineApiKey
);

// Always build PTB attempts (feature flag removed)
Expand All @@ -152,7 +157,8 @@ export class AttemptBuilder {
modelSpec: ModelSpec,
orgId: string,
bodyMapping: BodyMappingType = "OPENAI",
plugins?: Plugin[]
plugins?: Plugin[],
inlineApiKey?: string
): Promise<Attempt[]> {
// Get provider data once
const providerDataResult = registry.getModelProviderEntry(
Expand All @@ -179,7 +185,8 @@ export class AttemptBuilder {
providerData,
orgId,
bodyMapping,
plugins
plugins,
inlineApiKey
),
this.buildPtbAttempts(modelSpec, providerData, bodyMapping, plugins),
]);
Expand All @@ -192,7 +199,8 @@ export class AttemptBuilder {
providerData: ModelProviderEntry,
orgId: string,
bodyMapping: BodyMappingType = "OPENAI",
plugins?: Plugin[]
plugins?: Plugin[],
inlineApiKey?: string
): Promise<Attempt[]> {
// Get user's provider key
const keySpan = this.traceContext?.sampled
Expand All @@ -208,7 +216,7 @@ export class AttemptBuilder {
)
: null;

const userKey = await this.providerKeysManager.getProviderKeyWithFetch(
let userKey = await this.providerKeysManager.getProviderKeyWithFetch(
providerData.provider,
modelSpec.modelName,
orgId,
Expand All @@ -217,6 +225,19 @@ export class AttemptBuilder {

this.tracer.finishSpan(keySpan);

// If no stored key, fall back to inline API key from request Authorization header
if ((!userKey || !this.isByokEnabled(userKey)) && inlineApiKey) {
userKey = {
provider: providerData.provider,
org_id: orgId,
decrypted_provider_key: inlineApiKey,
decrypted_provider_secret_key: null,
auth_type: "key",
byok_enabled: true,
config: null,
};
}

if (!userKey || !this.isByokEnabled(userKey)) {
return []; // No BYOK available
}
Expand Down
13 changes: 12 additions & 1 deletion worker/src/lib/ai-gateway/SimpleAIGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,23 @@ export class SimpleAIGateway {
this.traceContext
)
: null;
// Pass the user's inline API key (from Authorization header) so it can be
// used as a fallback when no stored BYOK key exists in the database.
// Only pass if it's not a Helicone key (those are for Helicone auth, not provider auth).
const inlineApiKey =
this.apiKey &&
!this.apiKey.startsWith("sk-helicone-") &&
!this.apiKey.startsWith("pk-helicone-")
? this.apiKey
: undefined;

let attempts = await this.attemptBuilder.buildAttempts(
modelStrings,
this.orgId,
bodyMapping,
plugins,
globalIgnoreProviders
globalIgnoreProviders,
inlineApiKey
);
this.tracer.finishSpan(buildSpan);

Expand Down