From cb8083f5f8e3a188639fd119e30b6d7cb0c464b1 Mon Sep 17 00:00:00 2001 From: Matt Rubens <2600+mrubens@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:07:42 -0400 Subject: [PATCH 1/2] Remove the InferenceGateway feature flag and enable the gateway everywhere --- .../__tests__/inference-gateway.test.ts | 25 ---------------- apps/api/src/handlers/inference/index.ts | 19 ------------ .../__tests__/spawn-blaxel-worker.test.ts | 4 --- .../__tests__/spawn-daytona-worker.test.ts | 4 --- .../__tests__/spawn-modal-worker.test.ts | 4 --- .../inference-gateway-flag.ts | 11 ------- .../compute-providers/spawn-blaxel-worker.ts | 2 -- .../compute-providers/spawn-daytona-worker.ts | 2 -- .../compute-providers/spawn-docker-worker.ts | 2 -- .../src/compute-providers/spawn-e2b-worker.ts | 2 -- .../compute-providers/spawn-modal-worker.ts | 2 -- apps/web/src/lib/mock-utils.ts | 1 - .../src/worker-env/__tests__/base.test.ts | 24 +++++---------- .../compute-providers/src/worker-env/base.ts | 22 ++++++-------- .../src/worker-env/blaxel.ts | 2 -- .../src/worker-env/daytona.ts | 2 -- .../src/worker-env/docker.ts | 2 -- .../compute-providers/src/worker-env/e2b.ts | 2 -- .../compute-providers/src/worker-env/modal.ts | 2 -- .../compute-providers/src/worker-env/types.ts | 6 ---- packages/db/src/lib/model-runtime-config.ts | 6 ++-- packages/feature-flags/src/config.ts | 11 ------- packages/feature-flags/src/evaluator.ts | 29 +------------------ packages/feature-flags/src/server/index.ts | 1 - packages/feature-flags/src/types.ts | 1 - .../__tests__/dequeue-helpers.test.ts | 15 +--------- .../server/lib/task-runs/dequeue-helpers.ts | 9 ++---- packages/types/src/inference-gateway.ts | 4 +-- 28 files changed, 26 insertions(+), 190 deletions(-) delete mode 100644 apps/controller/src/compute-providers/inference-gateway-flag.ts diff --git a/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts b/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts index ac4503ed8..998a33729 100644 --- a/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts +++ b/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts @@ -7,14 +7,10 @@ const { mockFindTaskRun, mockResolveModelProviderEnvValue, mockGetFreshChatGptAccessToken, - mockIsInferenceGatewayEnabledForDeployment, } = vi.hoisted(() => ({ mockFindTaskRun: vi.fn(), mockResolveModelProviderEnvValue: vi.fn(), mockGetFreshChatGptAccessToken: vi.fn(), - mockIsInferenceGatewayEnabledForDeployment: vi.fn( - async (_redis?: unknown, _prefix?: string) => true, - ), })); vi.mock('@roomote/db/server', () => ({ @@ -29,15 +25,6 @@ vi.mock('@roomote/db/server', () => ({ getFreshChatGptAccessToken: mockGetFreshChatGptAccessToken, })); -vi.mock('@roomote/feature-flags/server', () => ({ - isInferenceGatewayEnabledForDeployment: (redis: unknown, prefix?: string) => - mockIsInferenceGatewayEnabledForDeployment(redis, prefix), -})); - -vi.mock('@roomote/redis', () => ({ - getRedis: vi.fn(() => ({})), -})); - import { inference } from '../index'; function createApp(authContext: Variables['authContext']) { @@ -102,7 +89,6 @@ describe('inference gateway', () => { beforeEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals(); - mockIsInferenceGatewayEnabledForDeployment.mockResolvedValue(true); mockFindTaskRun.mockResolvedValue({ id: 42 }); mockResolveModelProviderEnvValue.mockImplementation( async (names: string | readonly string[]) => { @@ -132,17 +118,6 @@ describe('inference gateway', () => { expect(fetchMock).not.toHaveBeenCalled(); }); - it('rejects requests when the gateway flag is disabled for the deployment', async () => { - const fetchMock = stubUpstreamFetch(); - mockIsInferenceGatewayEnabledForDeployment.mockResolvedValue(false); - - const response = await postMessages(createApp(createRunToken())); - - expect(response.status).toBe(403); - expect(mockFindTaskRun).not.toHaveBeenCalled(); - expect(fetchMock).not.toHaveBeenCalled(); - }); - it('rejects run tokens whose task run no longer exists', async () => { const fetchMock = stubUpstreamFetch(); mockFindTaskRun.mockResolvedValue(undefined); diff --git a/apps/api/src/handlers/inference/index.ts b/apps/api/src/handlers/inference/index.ts index fa5fc70cf..8148f29ca 100644 --- a/apps/api/src/handlers/inference/index.ts +++ b/apps/api/src/handlers/inference/index.ts @@ -2,8 +2,6 @@ import { Hono } from 'hono'; import { formatSingleLineLog } from '@roomote/types'; import { db, eq, taskRuns } from '@roomote/db/server'; -import { isInferenceGatewayEnabledForDeployment } from '@roomote/feature-flags/server'; -import { getRedis } from '@roomote/redis'; import type { Variables } from '../../types'; import { fetchWithLongLivedStreamDispatcher } from '../long-lived-fetch'; @@ -111,23 +109,6 @@ inference.on(['POST', 'GET'], '/:provider/*', async (c) => { ); } - // The gateway is only open when the deployment has opted into it. Without - // this check the endpoint would serve provider keys on every deployment - // regardless of the flag, so a run token could reach a provider key its - // sandbox never held. Fails closed (gateway closed) when evaluation is - // unavailable, matching the withholding side. - const gatewayEnabled = await isInferenceGatewayEnabledForDeployment( - getRedis(), - logPrefix, - ); - - if (!gatewayEnabled) { - return c.json( - { error: 'The inference gateway is not enabled for this deployment' }, - 403, - ); - } - const taskRun = await db.query.taskRuns.findFirst({ columns: { id: true }, where: eq(taskRuns.id, auth.runId), diff --git a/apps/controller/src/compute-providers/__tests__/spawn-blaxel-worker.test.ts b/apps/controller/src/compute-providers/__tests__/spawn-blaxel-worker.test.ts index 09089342b..27236df6d 100644 --- a/apps/controller/src/compute-providers/__tests__/spawn-blaxel-worker.test.ts +++ b/apps/controller/src/compute-providers/__tests__/spawn-blaxel-worker.test.ts @@ -16,10 +16,6 @@ const mockCreateComputeProviderClient = vi.fn((_arg?: unknown) => ({ })); const mockFindTask = vi.fn(); -vi.mock('../inference-gateway-flag', () => ({ - isInferenceGatewayEnabledForWorkerEnv: vi.fn(async () => false), -})); - vi.mock('@roomote/db/server', async (importOriginal) => { const actual = await importOriginal(); return { diff --git a/apps/controller/src/compute-providers/__tests__/spawn-daytona-worker.test.ts b/apps/controller/src/compute-providers/__tests__/spawn-daytona-worker.test.ts index 9f5251f9f..ef5ba0def 100644 --- a/apps/controller/src/compute-providers/__tests__/spawn-daytona-worker.test.ts +++ b/apps/controller/src/compute-providers/__tests__/spawn-daytona-worker.test.ts @@ -15,10 +15,6 @@ const mockGetNamedPortsForTaskRun = vi.fn(); const mockPrimeEnvironmentOidcForMachine = vi.fn(); const mockFindTask = vi.fn(); -vi.mock('../inference-gateway-flag', () => ({ - isInferenceGatewayEnabledForWorkerEnv: vi.fn(async () => false), -})); - vi.mock('@roomote/db/server', async (importOriginal) => { const actual = await importOriginal(); diff --git a/apps/controller/src/compute-providers/__tests__/spawn-modal-worker.test.ts b/apps/controller/src/compute-providers/__tests__/spawn-modal-worker.test.ts index 31580c007..0363b9105 100644 --- a/apps/controller/src/compute-providers/__tests__/spawn-modal-worker.test.ts +++ b/apps/controller/src/compute-providers/__tests__/spawn-modal-worker.test.ts @@ -35,10 +35,6 @@ function mockTaskRun( } as unknown as TaskRun; } -vi.mock('../inference-gateway-flag', () => ({ - isInferenceGatewayEnabledForWorkerEnv: vi.fn(async () => false), -})); - vi.mock('@roomote/db/server', async (importOriginal) => { const actual = await importOriginal(); diff --git a/apps/controller/src/compute-providers/inference-gateway-flag.ts b/apps/controller/src/compute-providers/inference-gateway-flag.ts deleted file mode 100644 index b44e3047b..000000000 --- a/apps/controller/src/compute-providers/inference-gateway-flag.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { isInferenceGatewayEnabledForDeployment } from '@roomote/feature-flags/server'; -import { getRedis } from '@roomote/redis'; - -/** - * Evaluate the deployment-level InferenceGateway flag for worker spawn env - * construction. Delegates to the shared fail-closed evaluator so the spawn - * env and the dequeue env are gated by identical logic. - */ -export async function isInferenceGatewayEnabledForWorkerEnv(): Promise { - return isInferenceGatewayEnabledForDeployment(getRedis(), '[spawn-worker]'); -} diff --git a/apps/controller/src/compute-providers/spawn-blaxel-worker.ts b/apps/controller/src/compute-providers/spawn-blaxel-worker.ts index f40d7ac2e..1efeaf209 100644 --- a/apps/controller/src/compute-providers/spawn-blaxel-worker.ts +++ b/apps/controller/src/compute-providers/spawn-blaxel-worker.ts @@ -19,7 +19,6 @@ import { resolveAuthBypassHeaderName, resolveAuthBypassValue, } from '@roomote/compute-providers'; -import { isInferenceGatewayEnabledForWorkerEnv } from './inference-gateway-flag'; import { primeEnvironmentOidcForMachine } from '../sandbox-oidc'; import { @@ -186,7 +185,6 @@ export async function spawnBlaxelWorker( args, env: buildBlaxelWorkerEnv({ authToken, - inferenceGatewayEnabled: await isInferenceGatewayEnabledForWorkerEnv(), sandboxExpiresAtMs: Date.now() + config.blaxelTimeoutMs, deploymentSlug: config.deploymentSlug, environmentId, diff --git a/apps/controller/src/compute-providers/spawn-daytona-worker.ts b/apps/controller/src/compute-providers/spawn-daytona-worker.ts index 8c2c5ea70..055e020aa 100644 --- a/apps/controller/src/compute-providers/spawn-daytona-worker.ts +++ b/apps/controller/src/compute-providers/spawn-daytona-worker.ts @@ -20,7 +20,6 @@ import { resolveAuthBypassHeaderName, resolveAuthBypassValue, } from '@roomote/compute-providers'; -import { isInferenceGatewayEnabledForWorkerEnv } from './inference-gateway-flag'; import { primeEnvironmentOidcForMachine } from '../sandbox-oidc'; import { @@ -331,7 +330,6 @@ export async function spawnDaytonaWorker( args, env: buildDaytonaWorkerEnv({ authToken, - inferenceGatewayEnabled: await isInferenceGatewayEnabledForWorkerEnv(), sandboxExpiresAtMs: Date.now() + daytonaTimeoutMs, deploymentSlug, environmentId, diff --git a/apps/controller/src/compute-providers/spawn-docker-worker.ts b/apps/controller/src/compute-providers/spawn-docker-worker.ts index a44c6b0a5..2ea6d2c83 100644 --- a/apps/controller/src/compute-providers/spawn-docker-worker.ts +++ b/apps/controller/src/compute-providers/spawn-docker-worker.ts @@ -23,7 +23,6 @@ import { resolveAuthBypassHeaderName, resolveAuthBypassValue, } from '@roomote/compute-providers'; -import { isInferenceGatewayEnabledForWorkerEnv } from './inference-gateway-flag'; import { getNamedPortsForTaskRun, @@ -366,7 +365,6 @@ export async function spawnDockerWorker( const workerEnv = buildDockerWorkerEnv({ authToken, - inferenceGatewayEnabled: await isInferenceGatewayEnabledForWorkerEnv(), sandboxExpiresAtMs: Date.now() + config.dockerTimeoutMs, deploymentSlug: config.deploymentSlug, environmentId: taskRun.payload.environmentId, diff --git a/apps/controller/src/compute-providers/spawn-e2b-worker.ts b/apps/controller/src/compute-providers/spawn-e2b-worker.ts index 893e157b3..65fc9c7bd 100644 --- a/apps/controller/src/compute-providers/spawn-e2b-worker.ts +++ b/apps/controller/src/compute-providers/spawn-e2b-worker.ts @@ -20,7 +20,6 @@ import { resolveAuthBypassHeaderName, resolveAuthBypassValue, } from '@roomote/compute-providers'; -import { isInferenceGatewayEnabledForWorkerEnv } from './inference-gateway-flag'; import { primeEnvironmentOidcForMachine } from '../sandbox-oidc'; import { @@ -327,7 +326,6 @@ export async function spawnE2bWorker( args, env: buildE2bWorkerEnv({ authToken, - inferenceGatewayEnabled: await isInferenceGatewayEnabledForWorkerEnv(), sandboxExpiresAtMs: Date.now() + e2bTimeoutMs, deploymentSlug, environmentId, diff --git a/apps/controller/src/compute-providers/spawn-modal-worker.ts b/apps/controller/src/compute-providers/spawn-modal-worker.ts index 47069b5e5..17b64e3e5 100644 --- a/apps/controller/src/compute-providers/spawn-modal-worker.ts +++ b/apps/controller/src/compute-providers/spawn-modal-worker.ts @@ -22,7 +22,6 @@ import { resolveAuthBypassHeaderName, resolveAuthBypassValue, } from '@roomote/compute-providers'; -import { isInferenceGatewayEnabledForWorkerEnv } from './inference-gateway-flag'; import { primeEnvironmentOidcForMachine } from '../sandbox-oidc'; import { @@ -410,7 +409,6 @@ export async function spawnModalWorker( args, env: buildModalWorkerEnv({ authToken, - inferenceGatewayEnabled: await isInferenceGatewayEnabledForWorkerEnv(), sandboxExpiresAtMs: Date.now() + modalTimeoutMs, deploymentSlug, environmentId, diff --git a/apps/web/src/lib/mock-utils.ts b/apps/web/src/lib/mock-utils.ts index a879975bf..e2d61aee1 100644 --- a/apps/web/src/lib/mock-utils.ts +++ b/apps/web/src/lib/mock-utils.ts @@ -11,7 +11,6 @@ export const mockFeatureFlags: Record = { [FeatureFlag.AuthorshipRules]: false, [FeatureFlag.BackgroundSubagents]: false, [FeatureFlag.CodeMode]: false, - [FeatureFlag.InferenceGateway]: false, }; export const mockUserResource: UserResource = { diff --git a/packages/compute-providers/src/worker-env/__tests__/base.test.ts b/packages/compute-providers/src/worker-env/__tests__/base.test.ts index c6a54d151..78b67666a 100644 --- a/packages/compute-providers/src/worker-env/__tests__/base.test.ts +++ b/packages/compute-providers/src/worker-env/__tests__/base.test.ts @@ -115,7 +115,7 @@ describe('buildBaseWorkerEnv', () => { expect(env.R_SMALL_MODEL).toBeUndefined(); }); - it('forwards deployment model config and provider keys to workers', () => { + it('forwards deployment model config and custom provider keys to workers', () => { process.env.R_MODEL = 'openrouter/openai/gpt-5.4'; process.env.R_SMALL_MODEL = 'openrouter/openai/gpt-5.4-mini'; process.env.R_VISION_MODEL = 'openrouter/openai/gpt-5.5'; @@ -150,27 +150,17 @@ describe('buildBaseWorkerEnv', () => { expect(env.R_EXPLORE_MODEL_REASONING_EFFORT).toBe('low'); expect(env.R_PLANNING_MODEL_REASONING_EFFORT).toBe('high'); expect(env.R_MODEL_ENV_KEYS).toBe('CUSTOM_PROVIDER_API_KEY'); - expect(env.OPENROUTER_API_KEY).toBe('openrouter-key'); + // Gateway-covered keys stay on the control plane; only custom provider + // credentials the gateway cannot serve reach the worker. + expect(env.OPENROUTER_API_KEY).toBeUndefined(); expect(env.CUSTOM_PROVIDER_API_KEY).toBe('custom-provider-key'); }); - it('forwards Vercel AI Gateway credentials through the shared provider key list', () => { - process.env.R_MODEL = 'vercel/openai/gpt-5.4'; - process.env.AI_GATEWAY_API_KEY = 'vercel-key'; - - const env = buildBaseWorkerEnv({ - authToken: 'auth-token', - extraEnv: {}, - }); - - expect(env.R_MODEL).toBe('vercel/openai/gpt-5.4'); - expect(env.AI_GATEWAY_API_KEY).toBe('vercel-key'); - }); - - it('holds back gateway-covered provider keys when the inference gateway is enabled', () => { + it('holds back gateway-covered provider keys from the worker env', () => { process.env.R_MODEL = 'anthropic/claude-sonnet-5'; process.env.ANTHROPIC_API_KEY = 'anthropic-key'; process.env.OPENROUTER_API_KEY = 'openrouter-key'; + process.env.AI_GATEWAY_API_KEY = 'vercel-key'; process.env.AWS_BEARER_TOKEN_BEDROCK = 'bedrock-key'; process.env.XAI_API_KEY = 'xai-key'; process.env.AWS_REGION = 'us-west-2'; @@ -178,12 +168,12 @@ describe('buildBaseWorkerEnv', () => { const env = buildBaseWorkerEnv({ authToken: 'auth-token', extraEnv: {}, - inferenceGatewayEnabled: true, }); expect(env.R_MODEL).toBe('anthropic/claude-sonnet-5'); expect(env.ANTHROPIC_API_KEY).toBeUndefined(); expect(env.OPENROUTER_API_KEY).toBeUndefined(); + expect(env.AI_GATEWAY_API_KEY).toBeUndefined(); expect(env.AWS_BEARER_TOKEN_BEDROCK).toBeUndefined(); expect(env.XAI_API_KEY).toBeUndefined(); // Region config is not a secret and Bedrock's Mantle merge still diff --git a/packages/compute-providers/src/worker-env/base.ts b/packages/compute-providers/src/worker-env/base.ts index 6bcce4af2..e7a2c447e 100644 --- a/packages/compute-providers/src/worker-env/base.ts +++ b/packages/compute-providers/src/worker-env/base.ts @@ -44,9 +44,7 @@ function getOperatorModelProviderEnvKeys(): string[] { return [...new Set([...DEFAULT_MODEL_PROVIDER_ENV_KEYS, ...configured])]; } -function buildOperatorModelProviderEnv( - inferenceGatewayEnabled: boolean, -): Record { +function buildOperatorModelProviderEnv(): Record { const env: Record = {}; for (const key of MODEL_ROLE_ENV_VAR_NAMES) { @@ -76,13 +74,12 @@ function buildOperatorModelProviderEnv( env.R_MODEL_ENV_KEYS = process.env.R_MODEL_ENV_KEYS; } - // When the inference gateway is enabled, gateway-covered provider keys - // (OpenRouter, Anthropic, OpenAI, Gemini, the aggregators, Bedrock) stay off - // the worker daemon process env so they never appear in the sandbox's - // /proc; the per-task dequeue env routes the harness through the gateway - // instead. ChatGPT subscriptions use their dedicated run-token gateway - // route, while disabled-provider credentials such as Vertex are blocked - // from the worker env entirely. + // Gateway-covered provider keys (OpenRouter, Anthropic, OpenAI, Gemini, + // the aggregators, Bedrock) stay off the worker daemon process env so they + // never appear in the sandbox's /proc; the per-task dequeue env routes the + // harness through the inference gateway instead. ChatGPT subscriptions use + // their dedicated run-token gateway route, while disabled-provider + // credentials such as Vertex are blocked from the worker env entirely. const gatewayCoveredKeys = new Set(INFERENCE_GATEWAY_PROVIDER_ENV_VAR_NAMES); for (const key of getOperatorModelProviderEnvKeys()) { @@ -90,7 +87,7 @@ function buildOperatorModelProviderEnv( continue; } - if (inferenceGatewayEnabled && gatewayCoveredKeys.has(key)) { + if (gatewayCoveredKeys.has(key)) { continue; } @@ -108,7 +105,6 @@ export function buildBaseWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled = false, }: BuildWorkerEnvOptions): Record { const previewProxyBaseUrl = process.env.PREVIEW_PROXY_BASE_URL; @@ -157,7 +153,7 @@ export function buildBaseWorkerEnv({ ...(process.env.PREVIEW_AUTH_COOKIE_NAME && { PREVIEW_AUTH_COOKIE_NAME: process.env.PREVIEW_AUTH_COOKIE_NAME, }), - ...buildOperatorModelProviderEnv(inferenceGatewayEnabled), + ...buildOperatorModelProviderEnv(), ...filterWorkerExtraEnv(extraEnv), }; } diff --git a/packages/compute-providers/src/worker-env/blaxel.ts b/packages/compute-providers/src/worker-env/blaxel.ts index 0874dc7c7..a5918d063 100644 --- a/packages/compute-providers/src/worker-env/blaxel.ts +++ b/packages/compute-providers/src/worker-env/blaxel.ts @@ -6,7 +6,6 @@ export function buildBlaxelWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, deploymentSlug, environmentId, image, @@ -20,7 +19,6 @@ export function buildBlaxelWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, }), ...buildWorkerContextEnv({ provider: 'blaxel', diff --git a/packages/compute-providers/src/worker-env/daytona.ts b/packages/compute-providers/src/worker-env/daytona.ts index b4ad2377c..301581312 100644 --- a/packages/compute-providers/src/worker-env/daytona.ts +++ b/packages/compute-providers/src/worker-env/daytona.ts @@ -7,7 +7,6 @@ export function buildDaytonaWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, deploymentSlug, environmentId, snapshotName, @@ -21,7 +20,6 @@ export function buildDaytonaWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, }), ...buildWorkerContextEnv({ provider: 'daytona', diff --git a/packages/compute-providers/src/worker-env/docker.ts b/packages/compute-providers/src/worker-env/docker.ts index 6f05fcc77..aaa3fb03b 100644 --- a/packages/compute-providers/src/worker-env/docker.ts +++ b/packages/compute-providers/src/worker-env/docker.ts @@ -6,7 +6,6 @@ export function buildDockerWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, deploymentSlug, environmentId, image, @@ -20,7 +19,6 @@ export function buildDockerWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, }), ...buildWorkerContextEnv({ provider: 'docker', diff --git a/packages/compute-providers/src/worker-env/e2b.ts b/packages/compute-providers/src/worker-env/e2b.ts index e96b1a1d3..db6dd4437 100644 --- a/packages/compute-providers/src/worker-env/e2b.ts +++ b/packages/compute-providers/src/worker-env/e2b.ts @@ -7,7 +7,6 @@ export function buildE2bWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, deploymentSlug, environmentId, templateId, @@ -21,7 +20,6 @@ export function buildE2bWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, }), ...buildWorkerContextEnv({ provider: 'e2b', diff --git a/packages/compute-providers/src/worker-env/modal.ts b/packages/compute-providers/src/worker-env/modal.ts index 6bec6834e..865a83ad4 100644 --- a/packages/compute-providers/src/worker-env/modal.ts +++ b/packages/compute-providers/src/worker-env/modal.ts @@ -7,7 +7,6 @@ export function buildModalWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, deploymentSlug, environmentId, baseImageRef, @@ -21,7 +20,6 @@ export function buildModalWorkerEnv({ authToken, sandboxExpiresAtMs, extraEnv, - inferenceGatewayEnabled, }), ...buildWorkerContextEnv({ provider: 'modal', diff --git a/packages/compute-providers/src/worker-env/types.ts b/packages/compute-providers/src/worker-env/types.ts index ce56fccda..2985c7775 100644 --- a/packages/compute-providers/src/worker-env/types.ts +++ b/packages/compute-providers/src/worker-env/types.ts @@ -2,10 +2,4 @@ export type BuildWorkerEnvOptions = { authToken: string; sandboxExpiresAtMs?: number; extraEnv?: Record; - /** - * Evaluated InferenceGateway feature flag. When true, gateway-covered - * provider keys are withheld from the worker daemon env; the per-task - * dequeue env routes the harness through the gateway instead. - */ - inferenceGatewayEnabled?: boolean; }; diff --git a/packages/db/src/lib/model-runtime-config.ts b/packages/db/src/lib/model-runtime-config.ts index 6e3f3c1ce..230cef834 100644 --- a/packages/db/src/lib/model-runtime-config.ts +++ b/packages/db/src/lib/model-runtime-config.ts @@ -186,9 +186,9 @@ export async function resolveEffectiveModelRuntimeEnv( * Route sandbox inference through the gateway: the configured provider * keys the gateway can serve stay on the control plane and their names * are advertised via `R_INFERENCE_GATEWAY_KEYS` instead. Callers - * resolving env for the sandbox pass the evaluated InferenceGateway - * feature flag; control-plane callers (routing, titles, summaries) omit it - * because that inference holds no run token to present to the gateway. + * resolving env for the sandbox pass true; control-plane callers + * (routing, titles, summaries) omit it because that inference holds no + * run token to present to the gateway. */ inferenceGateway?: boolean; } = {}, diff --git a/packages/feature-flags/src/config.ts b/packages/feature-flags/src/config.ts index 374a0fb13..27f56f626 100644 --- a/packages/feature-flags/src/config.ts +++ b/packages/feature-flags/src/config.ts @@ -64,17 +64,6 @@ export const FEATURE_FLAG_CONFIG: FeatureFlagConfigMap = { 'Enable background subagents so the Task tool can launch subagents asynchronously via its background flag, and standard-task delivery ships the PR before visual proof instead of blocking on it. Off by default: proof runs foreground, before delivery.', }, - [FeatureFlag.InferenceGateway]: { - // Off by default: sandboxes receive raw provider API keys as before. - // When enabled, gateway-covered provider keys stay on the control plane - // and task sandboxes route inference through /api/inference with their - // run-scoped token. - defaultValue: false, - metadataKey: 'inference_gateway', - description: - 'Route sandbox inference through the platform inference gateway so provider API keys and ChatGPT subscription auth stay on the control plane instead of entering task sandboxes', - }, - [FeatureFlag.CodeMode]: { // Off by default: keeps every MCP tool schema in the agent tool list. When // enabled, the worker injects OPENCODE_EXPERIMENTAL_CODE_MODE so OpenCode diff --git a/packages/feature-flags/src/evaluator.ts b/packages/feature-flags/src/evaluator.ts index 0875ea3be..a392cdbc9 100644 --- a/packages/feature-flags/src/evaluator.ts +++ b/packages/feature-flags/src/evaluator.ts @@ -14,7 +14,7 @@ import { } from './index'; import { MetadataCache } from './cache'; import { - FeatureFlag, + type FeatureFlag, type FeatureFlagContext, type FeatureFlagValues, type MetadataRecord, @@ -124,30 +124,3 @@ export function getFeatureFlagEvaluator(redis: Redis): FeatureFlagEvaluator { export function resetFeatureFlagEvaluatorForTests(): void { evaluatorInstance = null; } - -/** - * Evaluate the deployment-level InferenceGateway flag, failing closed - * (returns false: direct provider keys, the long-standing behavior) when flag - * evaluation is unavailable so a Redis outage cannot break task inference. - * Shared by the dequeue env resolver and every worker-spawn env builder so - * the two halves of the sandbox env are never gated by divergent logic. - */ -export async function isInferenceGatewayEnabledForDeployment( - redis: Redis, - logPrefix = '[inference-gateway]', -): Promise { - try { - return await getFeatureFlagEvaluator(redis).evaluate( - FeatureFlag.InferenceGateway, - { isDeploymentContext: true }, - ); - } catch (error) { - console.warn( - `${logPrefix} InferenceGateway flag evaluation failed; using direct provider keys: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - - return false; - } -} diff --git a/packages/feature-flags/src/server/index.ts b/packages/feature-flags/src/server/index.ts index b9f73e106..b07e1ba31 100644 --- a/packages/feature-flags/src/server/index.ts +++ b/packages/feature-flags/src/server/index.ts @@ -8,7 +8,6 @@ export { FeatureFlagEvaluator, getFeatureFlagEvaluator, - isInferenceGatewayEnabledForDeployment, resetFeatureFlagEvaluatorForTests, } from '../evaluator'; export { MetadataCache } from '../cache'; diff --git a/packages/feature-flags/src/types.ts b/packages/feature-flags/src/types.ts index 73e3ee91d..1d8f8190f 100644 --- a/packages/feature-flags/src/types.ts +++ b/packages/feature-flags/src/types.ts @@ -11,7 +11,6 @@ export enum FeatureFlag { AuthorshipRules = 'AuthorshipRules', BackgroundSubagents = 'BackgroundSubagents', CodeMode = 'CodeMode', - InferenceGateway = 'InferenceGateway', } export type FeatureFlagValue = diff --git a/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts b/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts index 536ac4226..cc3f45b47 100644 --- a/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts +++ b/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts @@ -11,7 +11,6 @@ const { mockResolveEffectiveModelRuntimeEnv, mockTaskRunsFindFirst, mockNotifySourceRunOnSettle, - mockIsInferenceGatewayEnabledForDeployment, } = vi.hoisted(() => ({ mockDecryptSecrets: vi.fn(), mockEnvironmentVariablesFindMany: vi.fn(), @@ -22,9 +21,6 @@ const { mockResolveEffectiveModelRuntimeEnv: vi.fn(), mockTaskRunsFindFirst: vi.fn(), mockNotifySourceRunOnSettle: vi.fn(), - mockIsInferenceGatewayEnabledForDeployment: vi.fn( - async (_redis?: unknown, _prefix?: string) => false, - ), })); vi.mock('@roomote/db/encryption', () => ({ @@ -92,15 +88,6 @@ vi.mock('@roomote/cloud-agents/server', () => ({ releaseTaskRun: vi.fn(), })); -vi.mock('@roomote/feature-flags/server', () => ({ - isInferenceGatewayEnabledForDeployment: (redis: unknown, prefix?: string) => - mockIsInferenceGatewayEnabledForDeployment(redis, prefix), -})); - -vi.mock('@roomote/redis', () => ({ - getRedis: vi.fn(() => ({})), -})); - vi.mock('../notify-source-run-on-settle', () => ({ notifySourceRunOnSettle: (...args: unknown[]) => mockNotifySourceRunOnSettle(...args), @@ -614,7 +601,7 @@ describe('fetchResolvedRuntimeEnvVars', () => { expect(envVars.R_INFERENCE_GATEWAY_KEYS).toBe('ANTHROPIC_API_KEY'); }); - it('admits only resolver-selected provider keys when the gateway is off', async () => { + it('admits only resolver-selected provider keys when the resolver returns raw keys', async () => { mockResolveEffectiveModelRuntimeEnv.mockResolvedValueOnce({ R_MODEL: 'anthropic/claude-test', ANTHROPIC_API_KEY: 'sk-ant', diff --git a/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts b/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts index 6731e1c6f..032d09841 100644 --- a/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts +++ b/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts @@ -28,8 +28,6 @@ import { sql, } from '@roomote/db/server'; import { decryptSecrets } from '@roomote/db/encryption'; -import { isInferenceGatewayEnabledForDeployment } from '@roomote/feature-flags/server'; -import { getRedis } from '@roomote/redis'; import { createTaskRunWorkerGitHubToken } from '@roomote/github'; import { createTaskRunScopedGitLabTokens } from '@roomote/gitlab'; import { createTaskRunBitbucketCredentials } from '@roomote/bitbucket'; @@ -252,10 +250,9 @@ export async function fetchResolvedRuntimeEnvVars( deploymentEnvVars ?? (await loadPersistedDeploymentEnvVarsFromDb()); const resolvedModelRuntimeEnv = await resolveEffectiveModelRuntimeEnv({ deploymentEnvVars: envVars, - inferenceGateway: await isInferenceGatewayEnabledForDeployment( - getRedis(), - '[fetchResolvedRuntimeEnvVars]', - ), + // Sandbox-bound env always routes inference through the gateway so + // provider keys stay on the control plane. + inferenceGateway: true, }); return redactControlPlaneEnvVars( diff --git a/packages/types/src/inference-gateway.ts b/packages/types/src/inference-gateway.ts index 352c8fa97..943417cfb 100644 --- a/packages/types/src/inference-gateway.ts +++ b/packages/types/src/inference-gateway.ts @@ -3,8 +3,8 @@ import type { SetupModelProviderId } from './model-provider-config'; /** * Sandbox-facing env var carrying the gateway base URL (e.g. * `https://api.example.com/api/inference`). Emitted by the control plane at - * dequeue when the InferenceGateway feature flag is enabled; its presence is - * what switches the worker's OpenCode config onto the gateway. + * dequeue; its presence is what switches the worker's OpenCode config onto + * the gateway. */ export const INFERENCE_GATEWAY_URL_ENV_VAR_NAME = 'R_INFERENCE_GATEWAY_URL'; From d8378616196b928495357f997218043a4b7ef71a Mon Sep 17 00:00:00 2001 From: Matt Rubens <2600+mrubens@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:13:05 -0400 Subject: [PATCH 2/2] Split resolver into sandbox and control-plane entry points instead of a mode boolean --- .../db/src/lib/model-runtime-config.test.ts | 25 ++++----- packages/db/src/lib/model-runtime-config.ts | 52 +++++++++++++------ .../__tests__/dequeue-helpers.test.ts | 18 +++---- .../server/lib/task-runs/dequeue-helpers.ts | 9 ++-- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/packages/db/src/lib/model-runtime-config.test.ts b/packages/db/src/lib/model-runtime-config.test.ts index f6d426fbc..ef06eebbc 100644 --- a/packages/db/src/lib/model-runtime-config.test.ts +++ b/packages/db/src/lib/model-runtime-config.test.ts @@ -43,7 +43,10 @@ vi.mock('../schema', () => ({ eq: vi.fn(), })); -import { resolveEffectiveModelRuntimeEnv } from './model-runtime-config'; +import { + resolveEffectiveModelRuntimeEnv, + resolveSandboxModelRuntimeEnv, +} from './model-runtime-config'; describe('resolveEffectiveModelRuntimeEnv', () => { beforeEach(() => { @@ -628,8 +631,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { }), ); - const env = await resolveEffectiveModelRuntimeEnv({ - inferenceGateway: true, + const env = await resolveSandboxModelRuntimeEnv({ runtimeEnv: {}, deploymentEnvVars: {}, }); @@ -646,8 +648,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { }); mockResolveOpenCodeAuthContent.mockResolvedValue(null); - const env = await resolveEffectiveModelRuntimeEnv({ - inferenceGateway: true, + const env = await resolveSandboxModelRuntimeEnv({ runtimeEnv: {}, deploymentEnvVars: {}, }); @@ -692,8 +693,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { }); it('withholds gateway-served keys and advertises them by name when enabled', async () => { - const env = await resolveEffectiveModelRuntimeEnv({ - inferenceGateway: true, + const env = await resolveSandboxModelRuntimeEnv({ runtimeEnv: {}, deploymentEnvVars: { ANTHROPIC_API_KEY: 'sk-anthropic' }, }); @@ -705,7 +705,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { expect(env.R_INFERENCE_GATEWAY_KEYS).toBe('ANTHROPIC_API_KEY'); }); - it('keeps raw keys when the gateway option is not passed', async () => { + it('keeps raw keys for control-plane resolution', async () => { const env = await resolveEffectiveModelRuntimeEnv({ runtimeEnv: {}, deploymentEnvVars: { ANTHROPIC_API_KEY: 'sk-anthropic' }, @@ -720,8 +720,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { // set it is neither served nor advertised — the dequeue redaction keys // off the advertised set, so a stray OPENAI_API_KEY the deployment set // for its own code survives into the sandbox. - const env = await resolveEffectiveModelRuntimeEnv({ - inferenceGateway: true, + const env = await resolveSandboxModelRuntimeEnv({ runtimeEnv: {}, deploymentEnvVars: { ANTHROPIC_API_KEY: 'sk-anthropic', @@ -771,8 +770,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { }), ); - const env = await resolveEffectiveModelRuntimeEnv({ - inferenceGateway: true, + const env = await resolveSandboxModelRuntimeEnv({ runtimeEnv: {}, deploymentEnvVars: { OPENROUTER_API_KEY: 'sk-openrouter', @@ -790,8 +788,7 @@ describe('resolveEffectiveModelRuntimeEnv', () => { }); it('drops disabled-provider credentials even when explicitly requested', async () => { - const env = await resolveEffectiveModelRuntimeEnv({ - inferenceGateway: true, + const env = await resolveSandboxModelRuntimeEnv({ runtimeEnv: { R_MODEL_ENV_KEYS: 'ANTHROPIC_API_KEY,AWS_BEARER_TOKEN_BEDROCK,GOOGLE_APPLICATION_CREDENTIALS,MISTRAL_API_KEY', diff --git a/packages/db/src/lib/model-runtime-config.ts b/packages/db/src/lib/model-runtime-config.ts index 230cef834..ad90b3079 100644 --- a/packages/db/src/lib/model-runtime-config.ts +++ b/packages/db/src/lib/model-runtime-config.ts @@ -177,21 +177,39 @@ export async function resolveModelProviderEnvValue( return undefined; } +type ModelRuntimeEnvOptions = { + runtimeEnv?: Partial>; + deploymentEnvVars?: Record; + executor?: DatabaseOrTransaction; +}; + +/** + * Resolve model runtime env for control-plane inference (routing, titles, + * summaries): raw provider keys are returned because control-plane calls + * hold no run token to present to the inference gateway. + */ export async function resolveEffectiveModelRuntimeEnv( - options: { - runtimeEnv?: Partial>; - deploymentEnvVars?: Record; - executor?: DatabaseOrTransaction; - /** - * Route sandbox inference through the gateway: the configured provider - * keys the gateway can serve stay on the control plane and their names - * are advertised via `R_INFERENCE_GATEWAY_KEYS` instead. Callers - * resolving env for the sandbox pass true; control-plane callers - * (routing, titles, summaries) omit it because that inference holds no - * run token to present to the gateway. - */ - inferenceGateway?: boolean; - } = {}, + options: ModelRuntimeEnvOptions = {}, +): Promise> { + return resolveModelRuntimeEnv(options, { inferenceGateway: false }); +} + +/** + * Resolve model runtime env for a task sandbox: the configured provider keys + * the inference gateway can serve stay on the control plane and their names + * are advertised via `R_INFERENCE_GATEWAY_KEYS` instead, and a connected + * ChatGPT subscription is routed through the gateway rather than + * materializing `OPENCODE_AUTH_CONTENT` in the sandbox. + */ +export async function resolveSandboxModelRuntimeEnv( + options: ModelRuntimeEnvOptions = {}, +): Promise> { + return resolveModelRuntimeEnv(options, { inferenceGateway: true }); +} + +async function resolveModelRuntimeEnv( + options: ModelRuntimeEnvOptions, + { inferenceGateway }: { inferenceGateway: boolean }, ): Promise> { const runtimeEnv = options.runtimeEnv ?? process.env; const executor = options.executor ?? db; @@ -319,7 +337,7 @@ export async function resolveEffectiveModelRuntimeEnv( // providers. Gateway coverage must therefore include both sets up front; // otherwise a later model switch either sees a missing key or uses a raw // provider credential that was already written into the sandbox. - const gatewaySwitchableModelIds = options.inferenceGateway + const gatewaySwitchableModelIds = inferenceGateway ? enabledCatalogModels.map((model) => model.id) : []; const gatewayProviderKeyNames = [ @@ -337,7 +355,7 @@ export async function resolveEffectiveModelRuntimeEnv( // gateway URL from its own platform URL and rebases exactly these providers. // Only configured keys are withheld; credentials for disabled providers are // filtered before this point and never flow to the task runtime. - const gatewayServedKeyNames = options.inferenceGateway + const gatewayServedKeyNames = inferenceGateway ? gatewayProviderKeyNames.filter( (name) => isInferenceGatewayCoveredEnvVar(name) && @@ -385,7 +403,7 @@ export async function resolveEffectiveModelRuntimeEnv( ? await resolveOpenCodeAuthContent({ executor }) : null; const routeChatGptThroughGateway = - options.inferenceGateway === true && injectedOpenCodeAuthContent != null; + inferenceGateway && injectedOpenCodeAuthContent != null; return { ...(resolvedRoomoteModel && { R_MODEL: resolvedRoomoteModel }), diff --git a/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts b/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts index cc3f45b47..9217c9141 100644 --- a/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts +++ b/packages/sdk/src/server/lib/task-runs/__tests__/dequeue-helpers.test.ts @@ -8,7 +8,7 @@ const { mockCreateTaskRunScopedGitLabTokens, mockCreateTaskRunGiteaCredentials, mockCreateTaskRunAdoCredentials, - mockResolveEffectiveModelRuntimeEnv, + mockResolveSandboxModelRuntimeEnv, mockTaskRunsFindFirst, mockNotifySourceRunOnSettle, } = vi.hoisted(() => ({ @@ -18,7 +18,7 @@ const { mockCreateTaskRunScopedGitLabTokens: vi.fn(), mockCreateTaskRunGiteaCredentials: vi.fn(), mockCreateTaskRunAdoCredentials: vi.fn(), - mockResolveEffectiveModelRuntimeEnv: vi.fn(), + mockResolveSandboxModelRuntimeEnv: vi.fn(), mockTaskRunsFindFirst: vi.fn(), mockNotifySourceRunOnSettle: vi.fn(), })); @@ -54,8 +54,8 @@ vi.mock('@roomote/db/server', () => ({ // fall through to the GitHub default. Provider-stamped payloads never reach // it. Individual tests override with mockResolvedValueOnce when needed. resolveWorkspaceSourceControlProvider: vi.fn(async () => undefined), - resolveEffectiveModelRuntimeEnv: (...args: unknown[]) => - mockResolveEffectiveModelRuntimeEnv(...args), + resolveSandboxModelRuntimeEnv: (...args: unknown[]) => + mockResolveSandboxModelRuntimeEnv(...args), inArray: vi.fn(), markTaskStartParallelCountEndedAt: vi.fn(), resolveTaskAttribution: vi.fn(), @@ -548,7 +548,7 @@ describe('redactControlPlaneEnvVars', () => { describe('fetchResolvedRuntimeEnvVars', () => { it('mirrors resolved model env to legacy ROOMOTE_* aliases for pre-rename snapshot workers', async () => { - mockResolveEffectiveModelRuntimeEnv.mockResolvedValueOnce({ + mockResolveSandboxModelRuntimeEnv.mockResolvedValueOnce({ R_MODEL: 'anthropic/claude-test', R_MODEL_REASONING_EFFORT: 'high', R_MODEL_ENV_KEYS: 'ANTHROPIC_API_KEY', @@ -571,7 +571,7 @@ describe('fetchResolvedRuntimeEnvVars', () => { }); it('derives legacy aliases from the resolved model env', async () => { - mockResolveEffectiveModelRuntimeEnv.mockResolvedValueOnce({ + mockResolveSandboxModelRuntimeEnv.mockResolvedValueOnce({ R_MODEL: 'anthropic/claude-test', }); @@ -584,7 +584,7 @@ describe('fetchResolvedRuntimeEnvVars', () => { }); it('admits no raw provider keys when the gateway is enabled', async () => { - mockResolveEffectiveModelRuntimeEnv.mockResolvedValueOnce({ + mockResolveSandboxModelRuntimeEnv.mockResolvedValueOnce({ R_MODEL: 'anthropic/claude-test', R_INFERENCE_GATEWAY_KEYS: 'ANTHROPIC_API_KEY', }); @@ -602,7 +602,7 @@ describe('fetchResolvedRuntimeEnvVars', () => { }); it('admits only resolver-selected provider keys when the resolver returns raw keys', async () => { - mockResolveEffectiveModelRuntimeEnv.mockResolvedValueOnce({ + mockResolveSandboxModelRuntimeEnv.mockResolvedValueOnce({ R_MODEL: 'anthropic/claude-test', ANTHROPIC_API_KEY: 'sk-ant', }); @@ -624,7 +624,7 @@ describe('fetchResolvedRuntimeEnvVars', () => { }); it('treats custom R_MODEL_ENV_KEYS credentials as resolver-managed', async () => { - mockResolveEffectiveModelRuntimeEnv.mockResolvedValueOnce({ + mockResolveSandboxModelRuntimeEnv.mockResolvedValueOnce({ R_MODEL: 'custom/test-model', R_MODEL_ENV_KEYS: 'CUSTOM_LLM_TOKEN', CUSTOM_LLM_TOKEN: 'selected-token', diff --git a/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts b/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts index 032d09841..80bd94f14 100644 --- a/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts +++ b/packages/sdk/src/server/lib/task-runs/dequeue-helpers.ts @@ -20,7 +20,7 @@ import { taskRuns, tasks, markTaskStartParallelCountEndedAt, - resolveEffectiveModelRuntimeEnv, + resolveSandboxModelRuntimeEnv, resolveWorkspaceSourceControlProvider, stringifyDecryptedEnvVarValue, syncTaskStateFromRuns, @@ -216,7 +216,7 @@ function withLegacySnapshotModelEnvAliases( /** * Removes model-runtime values from the generic deployment env stream. Model * configuration and provider credentials are reintroduced exclusively from - * resolveEffectiveModelRuntimeEnv below, making that resolver the allowlist + * resolveSandboxModelRuntimeEnv below, making that resolver the allowlist * boundary while preserving unrelated task variables from the shared table. * * R_MODEL_ENV_KEYS may name a custom provider credential that is not in the @@ -248,11 +248,8 @@ export async function fetchResolvedRuntimeEnvVars( ): Promise> { const envVars = deploymentEnvVars ?? (await loadPersistedDeploymentEnvVarsFromDb()); - const resolvedModelRuntimeEnv = await resolveEffectiveModelRuntimeEnv({ + const resolvedModelRuntimeEnv = await resolveSandboxModelRuntimeEnv({ deploymentEnvVars: envVars, - // Sandbox-bound env always routes inference through the gateway so - // provider keys stay on the control plane. - inferenceGateway: true, }); return redactControlPlaneEnvVars(