diff --git a/apps/worker/src/run-task/agent-home.ts b/apps/worker/src/run-task/agent-home.ts index 6415accc0..f269e93b2 100644 --- a/apps/worker/src/run-task/agent-home.ts +++ b/apps/worker/src/run-task/agent-home.ts @@ -27,9 +27,7 @@ import { type InferenceGatewayProvider, isConfiguredEnvValue, isTaskModelIdDisabled, - mergeAmazonBedrockProviderConfig, - mergeBedrockMantleOpenAiProviderConfig, - mergeBedrockMantleProviderConfig, + mergeBedrockProviderConfigs, mergeOpenAiCompatibleProviderConfig, mergeOpenCodeModelReasoningOptions, mergeOpenCodeChatGptFastModeOptions, @@ -1617,23 +1615,15 @@ function resolveModelBackedOpenCodeConfig( const providerConfig = mergeInferenceGatewayProviderConfig( mergeOpenCodeGoProviderConfig( mergeAzureCognitiveServicesProviderConfig( - mergeAmazonBedrockProviderConfig( - mergeBedrockMantleProviderConfig( - mergeBedrockMantleOpenAiProviderConfig( - mergeOpenAiCompatibleProviderConfig( - mergeOpenRouterVariantAliasModels( - providerModelConfig, - variantAliases, - ), - runtimeEnv, - configuredModelIds, - visionModel ?? effectiveCodingModel, - ), - runtimeEnv, - configuredModelIds, + mergeBedrockProviderConfigs( + mergeOpenAiCompatibleProviderConfig( + mergeOpenRouterVariantAliasModels( + providerModelConfig, + variantAliases, ), runtimeEnv, configuredModelIds, + visionModel ?? effectiveCodingModel, ), runtimeEnv, configuredModelIds, diff --git a/packages/cloud-agents/src/server/opencode-runtime.ts b/packages/cloud-agents/src/server/opencode-runtime.ts index d9a49b792..bfd6f59b7 100644 --- a/packages/cloud-agents/src/server/opencode-runtime.ts +++ b/packages/cloud-agents/src/server/opencode-runtime.ts @@ -7,9 +7,7 @@ import { CHATGPT_FAST_MODE_ENV_VAR_NAME, DISABLED_MODEL_PROVIDER_ENV_VAR_NAMES, isTaskModelIdDisabled, - mergeAmazonBedrockProviderConfig, - mergeBedrockMantleOpenAiProviderConfig, - mergeBedrockMantleProviderConfig, + mergeBedrockProviderConfigs, mergeOpenAiCompatibleProviderConfig, mergeOpenCodeModelReasoningOptions, mergeOpenCodeChatGptFastModeOptions, @@ -130,23 +128,12 @@ function buildModelBackedOpenCodeConfigContent( // Same Bedrock provider registrations the task worker applies: OpenCode's // catalog knows neither Mantle endpoint, and the native provider does not // read the deployment's bearer token on its own. - const providerConfig = mergeAmazonBedrockProviderConfig( - mergeBedrockMantleProviderConfig( - mergeBedrockMantleOpenAiProviderConfig( - mergeOpenAiCompatibleProviderConfig( - mergeOpenRouterVariantAliasModels( - providerModelConfig, - variantAliases, - ), - env, - configuredModelIds, - visionModel, - ), - env, - configuredModelIds, - ), + const providerConfig = mergeBedrockProviderConfigs( + mergeOpenAiCompatibleProviderConfig( + mergeOpenRouterVariantAliasModels(providerModelConfig, variantAliases), env, configuredModelIds, + visionModel, ), env, configuredModelIds, @@ -328,16 +315,8 @@ function mergeBedrockRegistrationsIntoConfigContent( !Array.isArray(config.provider) ? (config.provider as Record) : {}; - const provider = mergeAmazonBedrockProviderConfig( - mergeBedrockMantleProviderConfig( - mergeBedrockMantleOpenAiProviderConfig( - existingProvider, - env, - roleModelIds, - ), - env, - roleModelIds, - ), + const provider = mergeBedrockProviderConfigs( + existingProvider, env, roleModelIds, ); diff --git a/packages/types/src/bedrock-opencode-provider.test.ts b/packages/types/src/bedrock-opencode-provider.test.ts new file mode 100644 index 000000000..0e610d224 --- /dev/null +++ b/packages/types/src/bedrock-opencode-provider.test.ts @@ -0,0 +1,68 @@ +import { mergeBedrockProviderConfigs } from './bedrock-opencode-provider'; + +describe('mergeBedrockProviderConfigs', () => { + it('registers each transport while preserving its option precedence', () => { + const providerConfig = mergeBedrockProviderConfigs( + { + 'bedrock-mantle': { + options: { + apiKey: 'existing-mantle-key', + baseURL: 'https://existing-mantle.example.com', + custom: true, + }, + models: { + 'anthropic.claude-sonnet-5': { name: 'Existing Claude name' }, + }, + }, + 'bedrock-mantle-openai': { + options: { + apiKey: 'existing-openai-key', + baseURL: 'https://existing-openai.example.com', + }, + }, + 'amazon-bedrock': { + options: { apiKey: 'existing-native-key', custom: true }, + }, + }, + { AWS_REGION: 'eu-west-1' }, + [ + 'bedrock-mantle/anthropic.claude-sonnet-5', + 'bedrock-mantle-openai/openai.gpt-5.6-luna', + 'amazon-bedrock/amazon.nova-2-lite-v1:0', + ], + ); + + expect(providerConfig).toMatchObject({ + 'bedrock-mantle': { + npm: '@ai-sdk/anthropic', + options: { + apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', + baseURL: 'https://bedrock-mantle.eu-west-1.api.aws/anthropic/v1', + custom: true, + }, + models: { + 'anthropic.claude-sonnet-5': { name: 'Existing Claude name' }, + }, + }, + 'bedrock-mantle-openai': { + npm: '@ai-sdk/openai', + options: { + apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', + baseURL: 'https://bedrock-mantle.eu-west-1.api.aws/openai/v1', + }, + models: { + 'openai.gpt-5.6-luna': { name: 'openai.gpt-5.6-luna' }, + }, + }, + 'amazon-bedrock': { + npm: '@ai-sdk/amazon-bedrock', + options: { apiKey: 'existing-native-key', custom: true }, + models: { + 'amazon.nova-2-lite-v1:0': { + name: 'amazon.nova-2-lite-v1:0', + }, + }, + }, + }); + }); +}); diff --git a/packages/types/src/bedrock-opencode-provider.ts b/packages/types/src/bedrock-opencode-provider.ts index 6546ca978..a68837bd4 100644 --- a/packages/types/src/bedrock-opencode-provider.ts +++ b/packages/types/src/bedrock-opencode-provider.ts @@ -57,6 +57,59 @@ function collectModelIdsForPrefix( ]; } +type BedrockProviderRegistration = { + mergeOptions: ( + existingOptions: Record, + region: string, + ) => Record; + npm: string; + providerId: string; +}; + +function mergeBedrockProviderConfig( + providerConfig: Record, + runtimeEnv: RuntimeEnv, + modelIds: Array, + registration: BedrockProviderRegistration, +): Record { + const registeredModelIds = collectModelIdsForPrefix( + modelIds, + registration.providerId, + ); + + if (registeredModelIds.length === 0) { + return providerConfig; + } + + const region = resolveBedrockRegion(runtimeEnv); + const existingProvider = asRecord(providerConfig[registration.providerId]); + const existingOptions = asRecord(existingProvider.options); + const existingModels = asRecord(existingProvider.models); + const registeredModels = Object.fromEntries( + registeredModelIds.map((modelId) => [ + modelId, + { + name: modelId, + ...asRecord(existingModels[modelId]), + }, + ]), + ); + + return { + ...providerConfig, + [registration.providerId]: { + ...existingProvider, + npm: registration.npm, + name: 'Amazon Bedrock', + options: registration.mergeOptions(existingOptions, region), + models: { + ...existingModels, + ...registeredModels, + }, + }, + }; +} + /** * Bedrock Mantle serves GPT models through its OpenAI-compatible endpoint * (Responses API), not the Anthropic Messages endpoint the `bedrock-mantle` @@ -94,48 +147,15 @@ export function mergeBedrockMantleProviderConfig( runtimeEnv: RuntimeEnv, modelIds: Array, ): Record { - const mantleModelIds = collectModelIdsForPrefix( - modelIds, - BEDROCK_MANTLE_OPENCODE_PROVIDER_ID, - ); - - if (mantleModelIds.length === 0) { - return providerConfig; - } - - const region = resolveBedrockRegion(runtimeEnv); - const existingProvider = asRecord( - providerConfig[BEDROCK_MANTLE_OPENCODE_PROVIDER_ID], - ); - const existingOptions = asRecord(existingProvider.options); - const existingModels = asRecord(existingProvider.models); - const models = Object.fromEntries( - mantleModelIds.map((modelId) => [ - modelId, - { - name: modelId, - ...asRecord(existingModels[modelId]), - }, - ]), - ); - - return { - ...providerConfig, - [BEDROCK_MANTLE_OPENCODE_PROVIDER_ID]: { - ...existingProvider, - npm: '@ai-sdk/anthropic', - name: 'Amazon Bedrock', - options: { - ...existingOptions, - baseURL: `https://bedrock-mantle.${region}.api.aws/anthropic/v1`, - apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', - }, - models: { - ...existingModels, - ...models, - }, - }, - }; + return mergeBedrockProviderConfig(providerConfig, runtimeEnv, modelIds, { + providerId: BEDROCK_MANTLE_OPENCODE_PROVIDER_ID, + npm: '@ai-sdk/anthropic', + mergeOptions: (existingOptions, region) => ({ + ...existingOptions, + baseURL: `https://bedrock-mantle.${region}.api.aws/anthropic/v1`, + apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', + }), + }); } /** @@ -148,49 +168,17 @@ export function mergeBedrockMantleOpenAiProviderConfig( runtimeEnv: RuntimeEnv, modelIds: Array, ): Record { - const mantleModelIds = collectModelIdsForPrefix( - modelIds, - BEDROCK_MANTLE_OPENAI_OPENCODE_PROVIDER_ID, - ); - - if (mantleModelIds.length === 0) { - return providerConfig; - } - - const region = resolveBedrockRegion(runtimeEnv); - const existingProvider = asRecord( - providerConfig[BEDROCK_MANTLE_OPENAI_OPENCODE_PROVIDER_ID], - ); - const existingOptions = asRecord(existingProvider.options); - const existingModels = asRecord(existingProvider.models); - - return { - ...providerConfig, - [BEDROCK_MANTLE_OPENAI_OPENCODE_PROVIDER_ID]: { - ...existingProvider, - // Mantle GPT models support the OpenAI Responses API, not Chat - // Completions. The native provider selects the Responses transport. - npm: '@ai-sdk/openai', - name: 'Amazon Bedrock', - options: { - ...existingOptions, - baseURL: `https://bedrock-mantle.${region}.api.aws/openai/v1`, - apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', - }, - models: { - ...existingModels, - ...Object.fromEntries( - mantleModelIds.map((modelId) => [ - modelId, - { - name: modelId, - ...asRecord(existingModels[modelId]), - }, - ]), - ), - }, - }, - }; + return mergeBedrockProviderConfig(providerConfig, runtimeEnv, modelIds, { + providerId: BEDROCK_MANTLE_OPENAI_OPENCODE_PROVIDER_ID, + // Mantle GPT models support the OpenAI Responses API, not Chat + // Completions. The native provider selects the Responses transport. + npm: '@ai-sdk/openai', + mergeOptions: (existingOptions, region) => ({ + ...existingOptions, + baseURL: `https://bedrock-mantle.${region}.api.aws/openai/v1`, + apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', + }), + }); } /** @@ -203,45 +191,35 @@ export function mergeAmazonBedrockProviderConfig( runtimeEnv: RuntimeEnv, modelIds: Array, ): Record { - const bedrockModelIds = collectModelIdsForPrefix( - modelIds, - AMAZON_BEDROCK_OPENCODE_PROVIDER_ID, - ); + return mergeBedrockProviderConfig(providerConfig, runtimeEnv, modelIds, { + providerId: AMAZON_BEDROCK_OPENCODE_PROVIDER_ID, + npm: '@ai-sdk/amazon-bedrock', + mergeOptions: (existingOptions) => ({ + apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', + ...existingOptions, + }), + }); +} - if (bedrockModelIds.length === 0) { - return providerConfig; +/** Registers every Bedrock transport needed by the selected model ids. */ +export function mergeBedrockProviderConfigs( + providerConfig: Record, + runtimeEnv: RuntimeEnv, + modelIds: Array, +): Record { + let mergedProviderConfig = providerConfig; + + for (const mergeProviderConfig of [ + mergeBedrockMantleOpenAiProviderConfig, + mergeBedrockMantleProviderConfig, + mergeAmazonBedrockProviderConfig, + ]) { + mergedProviderConfig = mergeProviderConfig( + mergedProviderConfig, + runtimeEnv, + modelIds, + ); } - resolveBedrockRegion(runtimeEnv); - - const existingProvider = asRecord( - providerConfig[AMAZON_BEDROCK_OPENCODE_PROVIDER_ID], - ); - const existingOptions = asRecord(existingProvider.options); - const existingModels = asRecord(existingProvider.models); - - return { - ...providerConfig, - [AMAZON_BEDROCK_OPENCODE_PROVIDER_ID]: { - ...existingProvider, - npm: '@ai-sdk/amazon-bedrock', - name: 'Amazon Bedrock', - options: { - apiKey: '{env:AWS_BEARER_TOKEN_BEDROCK}', - ...existingOptions, - }, - models: { - ...existingModels, - ...Object.fromEntries( - bedrockModelIds.map((modelId) => [ - modelId, - { - name: modelId, - ...asRecord(existingModels[modelId]), - }, - ]), - ), - }, - }, - }; + return mergedProviderConfig; }