diff --git a/apps/dev/src/services/__tests__/ecosystem.test.ts b/apps/dev/src/services/__tests__/ecosystem.test.ts index fe9c54eca7..0ef6cb47b0 100644 --- a/apps/dev/src/services/__tests__/ecosystem.test.ts +++ b/apps/dev/src/services/__tests__/ecosystem.test.ts @@ -1,10 +1,47 @@ +import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; +import { + DEFAULT_MODEL_PROVIDER_CREDENTIAL_ENV_VAR_NAMES, + DEFAULT_MODEL_PROVIDER_ENV_KEYS, + isSettingsOnlyProviderEnvVar, +} from '@roomote/types'; + const require = createRequire(import.meta.url); const ecosystemConfigPath = fileURLToPath( new URL('../../../../../ecosystem.config.js', import.meta.url), ); +const productionComposePath = fileURLToPath( + new URL( + '../../../../../deploy/compose/docker-compose.prod.yml', + import.meta.url, + ), +); +const runtimeProviderEnvKeys = DEFAULT_MODEL_PROVIDER_ENV_KEYS.filter( + (key) => !isSettingsOnlyProviderEnvVar(key), +); +const workerLauncherEnvKeys = [ + 'R_MODEL', + 'R_SMALL_MODEL', + 'R_VISION_MODEL', + 'R_CODE_REVIEW_MODEL', + 'R_EXPLORE_MODEL', + 'R_PLANNING_MODEL', + 'R_MODEL_REASONING_EFFORT', + 'R_SMALL_MODEL_REASONING_EFFORT', + 'R_VISION_MODEL_REASONING_EFFORT', + 'R_CODE_REVIEW_MODEL_REASONING_EFFORT', + 'R_EXPLORE_MODEL_REASONING_EFFORT', + 'R_PLANNING_MODEL_REASONING_EFFORT', + 'R_MODEL_ENV_KEYS', + 'CUSTOM_PROVIDER_API_KEY', + 'AZURE_RESOURCE_NAME', + 'AZURE_COGNITIVE_SERVICES_RESOURCE_NAME', + 'AWS_REGION', + 'ZAI_REGION', + 'ZAI_CODING_PLAN_REGION', +] as const; interface EcosystemApp { name: string; @@ -122,6 +159,58 @@ describe('ecosystem.config.js', () => { }); }); + it('forwards every canonical runtime provider variable in deployment templates', () => { + process.env = { + ...originalEnv, + ...Object.fromEntries(runtimeProviderEnvKeys.map((key) => [key, key])), + }; + + const apps = loadEcosystemApps(); + for (const serviceName of ['api', 'web', 'bullmq', 'controller']) { + const app = apps.find((entry) => entry.name === `roomote-${serviceName}`); + + for (const key of runtimeProviderEnvKeys) { + expect(app?.env?.[key], `${serviceName} is missing ${key}`).toBe(key); + } + } + + const compose = readFileSync(productionComposePath, 'utf8'); + const inferenceEnv = compose.slice( + compose.indexOf('x-roomote-inference-env:'), + compose.indexOf('x-roomote-worker-launcher-env:'), + ); + for (const key of runtimeProviderEnvKeys) { + expect(inferenceEnv, `production Compose is missing ${key}`).toContain( + ` ${key}: \${${key}:-}`, + ); + } + }); + + it('gives the production controller only worker launcher model configuration', () => { + const compose = readFileSync(productionComposePath, 'utf8'); + const workerLauncherEnv = compose.slice( + compose.indexOf('x-roomote-worker-launcher-env:'), + compose.indexOf('x-roomote-web-env:'), + ); + const controllerEnv = compose.slice( + compose.indexOf('x-roomote-controller-env:'), + compose.indexOf('x-roomote-bullmq-env:'), + ); + + for (const key of workerLauncherEnvKeys) { + expect(workerLauncherEnv, `worker launcher is missing ${key}`).toContain( + ` ${key}: \${${key}:-}`, + ); + } + for (const key of DEFAULT_MODEL_PROVIDER_CREDENTIAL_ENV_VAR_NAMES) { + expect( + workerLauncherEnv, + `worker launcher must not receive provider credential ${key}`, + ).not.toContain(` ${key}:`); + } + expect(controllerEnv).toContain(' <<: *roomote-worker-launcher-env'); + }); + it('passes the preview inference launcher key only to the controller', () => { process.env = { ...originalEnv, diff --git a/deploy/compose/docker-compose.prod.yml b/deploy/compose/docker-compose.prod.yml index 65a8e2b22e..13ae03bb3f 100644 --- a/deploy/compose/docker-compose.prod.yml +++ b/deploy/compose/docker-compose.prod.yml @@ -75,6 +75,11 @@ x-roomote-inference-env: &roomote-inference-env CUSTOM_PROVIDER_API_KEY: ${CUSTOM_PROVIDER_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} + REQUESTY_API_KEY: ${REQUESTY_API_KEY:-} + AZURE_API_KEY: ${AZURE_API_KEY:-} + AZURE_RESOURCE_NAME: ${AZURE_RESOURCE_NAME:-} + AZURE_COGNITIVE_SERVICES_API_KEY: ${AZURE_COGNITIVE_SERVICES_API_KEY:-} + AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: ${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} @@ -92,6 +97,36 @@ x-roomote-inference-env: &roomote-inference-env OPENCODE_GO_API_KEY: ${OPENCODE_GO_API_KEY:-} BASETEN_API_KEY: ${BASETEN_API_KEY:-} TOGETHER_API_KEY: ${TOGETHER_API_KEY:-} + XAI_API_KEY: ${XAI_API_KEY:-} + OPENAI_COMPATIBLE_BASE_URL: ${OPENAI_COMPATIBLE_BASE_URL:-} + OPENAI_COMPATIBLE_API_KEY: ${OPENAI_COMPATIBLE_API_KEY:-} + LITELLM_BASE_URL: ${LITELLM_BASE_URL:-} + LITELLM_API_KEY: ${LITELLM_API_KEY:-} + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-} + VLLM_BASE_URL: ${VLLM_BASE_URL:-} + VLLM_API_KEY: ${VLLM_API_KEY:-} + +x-roomote-worker-launcher-env: &roomote-worker-launcher-env + <<: *roomote-base-env + R_MODEL: ${R_MODEL:-} + R_SMALL_MODEL: ${R_SMALL_MODEL:-} + R_VISION_MODEL: ${R_VISION_MODEL:-} + R_CODE_REVIEW_MODEL: ${R_CODE_REVIEW_MODEL:-} + R_EXPLORE_MODEL: ${R_EXPLORE_MODEL:-} + R_PLANNING_MODEL: ${R_PLANNING_MODEL:-} + R_MODEL_REASONING_EFFORT: ${R_MODEL_REASONING_EFFORT:-} + R_SMALL_MODEL_REASONING_EFFORT: ${R_SMALL_MODEL_REASONING_EFFORT:-} + R_VISION_MODEL_REASONING_EFFORT: ${R_VISION_MODEL_REASONING_EFFORT:-} + R_CODE_REVIEW_MODEL_REASONING_EFFORT: ${R_CODE_REVIEW_MODEL_REASONING_EFFORT:-} + R_EXPLORE_MODEL_REASONING_EFFORT: ${R_EXPLORE_MODEL_REASONING_EFFORT:-} + R_PLANNING_MODEL_REASONING_EFFORT: ${R_PLANNING_MODEL_REASONING_EFFORT:-} + R_MODEL_ENV_KEYS: ${R_MODEL_ENV_KEYS:-} + CUSTOM_PROVIDER_API_KEY: ${CUSTOM_PROVIDER_API_KEY:-} + AZURE_RESOURCE_NAME: ${AZURE_RESOURCE_NAME:-} + AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: ${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME:-} + AWS_REGION: ${AWS_REGION:-} + ZAI_REGION: ${ZAI_REGION:-} + ZAI_CODING_PLAN_REGION: ${ZAI_CODING_PLAN_REGION:-} x-roomote-web-env: &roomote-web-env <<: *roomote-inference-env @@ -212,7 +247,7 @@ x-roomote-api-env: &roomote-api-env R_MICROSOFT_TENANT_ID: ${R_MICROSOFT_TENANT_ID:-} x-roomote-controller-env: &roomote-controller-env - <<: *roomote-base-env + <<: *roomote-worker-launcher-env SANDBOX_OPENROUTER_API_KEY: ${SANDBOX_OPENROUTER_API_KEY:-} DATABASE_URL: ${DATABASE_URL:-postgres://postgres:roomote-postgres-password@postgres:5432/roomote} REDIS_URL: ${REDIS_URL:-redis://redis:6379} diff --git a/deploy/scripts/validate-compose-security.sh b/deploy/scripts/validate-compose-security.sh index cb8d33a7e0..85a011434f 100755 --- a/deploy/scripts/validate-compose-security.sh +++ b/deploy/scripts/validate-compose-security.sh @@ -18,6 +18,10 @@ export ENCRYPTION_KEY=12345678901234567890123456789012 export ARTIFACT_SIGNING_KEY=12345678901234567890123456789012 export DASHBOARD_PASSWORD=test-dashboard-password export ROOMOTE_APP_TMPFS_SIZE=768m +export R_MODEL=openrouter/openai/gpt-5.4 +export R_MODEL_ENV_KEYS=CUSTOM_PROVIDER_API_KEY +export CUSTOM_PROVIDER_API_KEY=test-custom-provider-key +export AWS_REGION=us-east-1 docker compose --profile local-inference -f "$compose_file" config --format json >"$rendered_config" @@ -79,6 +83,10 @@ jq -e ' "S3_SECRET_ACCESS_KEY", "R_GITHUB_APP_PRIVATE_KEY", "R_SLACK_CLIENT_SECRET", "OPENAI_API_KEY" ][] | . as $key | $root.services.controller.environment[$key] == null] | all) and + (.services.controller.environment.R_MODEL == "openrouter/openai/gpt-5.4") and + (.services.controller.environment.R_MODEL_ENV_KEYS == "CUSTOM_PROVIDER_API_KEY") and + (.services.controller.environment.CUSTOM_PROVIDER_API_KEY == "test-custom-provider-key") and + (.services.controller.environment.AWS_REGION == "us-east-1") and ([[ "JOB_AUTH_PRIVATE_KEY", "PREVIEW_AUTH_PRIVATE_KEY", "ENCRYPTION_KEY", "S3_SECRET_ACCESS_KEY", "OPENAI_API_KEY" diff --git a/ecosystem.config.js b/ecosystem.config.js index d3d1bc9036..a49eff424e 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -4,7 +4,12 @@ const useWorkerRelease = process.env.USE_WORKER_RELEASE === 'true'; const DEFAULT_OPENCODE_PROVIDER_ENV_KEYS = [ 'OPENROUTER_API_KEY', 'AI_GATEWAY_API_KEY', + 'REQUESTY_API_KEY', 'OPENAI_API_KEY', + 'AZURE_API_KEY', + 'AZURE_RESOURCE_NAME', + 'AZURE_COGNITIVE_SERVICES_API_KEY', + 'AZURE_COGNITIVE_SERVICES_RESOURCE_NAME', 'ANTHROPIC_API_KEY', 'MOONSHOT_API_KEY', 'KIMI_API_KEY', @@ -14,12 +19,21 @@ const DEFAULT_OPENCODE_PROVIDER_ENV_KEYS = [ 'ZAI_CODING_PLAN_API_KEY', 'ZAI_CODING_PLAN_REGION', 'OPENCODE_API_KEY', + 'OPENCODE_GO_API_KEY', 'BASETEN_API_KEY', 'TOGETHER_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'AWS_BEARER_TOKEN_BEDROCK', 'AWS_REGION', + 'XAI_API_KEY', + 'OPENAI_COMPATIBLE_BASE_URL', + 'OPENAI_COMPATIBLE_API_KEY', + 'LITELLM_BASE_URL', + 'LITELLM_API_KEY', + 'OLLAMA_BASE_URL', + 'VLLM_BASE_URL', + 'VLLM_API_KEY', ]; const OPENCODE_RUNTIME_ENV_KEYS = [ 'R_MODEL',