Skip to content

Commit 23c8111

Browse files
authored
Merge pull request #7555 from remix-project-org/aws2
Aws Bedrock
2 parents b91948c + a394de9 commit 23c8111

20 files changed

Lines changed: 718 additions & 52 deletions

File tree

apps/remix-ide/src/app/plugins/remixAI/ApiKeySettingsHelper.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,45 +61,64 @@ export class ApiKeySettingsHelper {
6161
*/
6262
async getUserApiKeysConfig(): Promise<IUserApiKeyConfig | undefined> {
6363
try {
64-
// First check if user has permission to use own API keys
64+
// Whether the user may swap the Remix proxy for their own keys on the
65+
// proxy-backed providers (anthropic / mistral / openai / moonshot).
6566
const hasPermission = await this.canUseOwnApiKeys()
66-
if (!hasPermission) {
67-
remixAILogger.log('[ApiKeySettingsHelper] User does not have permission to use own API keys')
68-
return undefined
69-
}
7067

71-
// Read settings via plugin calls (parallel for performance)
72-
const [useOwnKeysValue, anthropicApiKey, mistralApiKey, openaiApiKey, moonshotApiKey] = await Promise.all([
68+
// Read settings via plugin calls (parallel for performance). We read the
69+
// Bedrock API key regardless of `hasPermission`, because Bedrock has no
70+
// Remix proxy: if the user entered a key it must be used, there's no
71+
// proxy alternative to gate it against. The permission flag only governs
72+
// the proxy-backed providers below.
73+
const [
74+
useOwnKeysValue,
75+
anthropicApiKey,
76+
mistralApiKey,
77+
openaiApiKey,
78+
moonshotApiKey,
79+
bedrockBearerToken
80+
] = await Promise.all([
7381
this.getSetting('deepagent-api-keys-config'),
7482
this.getSetting('deepagent-anthropic-api-key'),
7583
this.getSetting('deepagent-mistral-api-key'),
7684
this.getSetting('deepagent-openai-api-key'),
77-
this.getSetting('deepagent-moonshot-api-key')
85+
this.getSetting('deepagent-moonshot-api-key'),
86+
this.getSetting('deepagent-bedrock-bearer-token')
7887
])
7988

8089
const useOwnKeys = useOwnKeysValue === 'true' || useOwnKeysValue === true
8190

91+
const hasBedrockKey = !!bedrockBearerToken
92+
93+
// Proxy-provider own keys are gated behind the permission; the Bedrock
94+
// key is not (see above).
95+
const anthropic = hasPermission ? String(anthropicApiKey || '') : ''
96+
const mistral = hasPermission ? String(mistralApiKey || '') : ''
97+
const openai = hasPermission ? String(openaiApiKey || '') : ''
98+
const moonshot = hasPermission ? String(moonshotApiKey || '') : ''
99+
const hasAnyProxyKey = !!(anthropic || mistral || openai || moonshot)
100+
82101
// Debug logging
83102
remixAILogger.log('[ApiKeySettingsHelper] Reading API keys from settings:', {
103+
hasPermission,
84104
useOwnKeys,
85-
hasAnthropicKey: !!anthropicApiKey,
86-
hasMistralKey: !!mistralApiKey,
87-
hasOpenaiKey: !!openaiApiKey,
88-
hasMoonshotKey: !!moonshotApiKey
105+
hasAnyProxyKey,
106+
hasBedrockKey
89107
})
90108

91-
// Auto-enable if any API key is set
92-
const hasAnyKey = anthropicApiKey || mistralApiKey || openaiApiKey || moonshotApiKey
93-
if (!useOwnKeys && !hasAnyKey) {
109+
// Nothing to contribute → callers fall back to the proxy.
110+
// - No Bedrock key AND no proxy-provider own keys in play.
111+
if (!hasBedrockKey && !hasAnyProxyKey && !(useOwnKeys && hasPermission)) {
94112
return undefined
95113
}
96114

97115
return {
98-
useOwnKeys: useOwnKeys || !!hasAnyKey,
99-
anthropicApiKey: String(anthropicApiKey || ''),
100-
mistralApiKey: String(mistralApiKey || ''),
101-
openaiApiKey: String(openaiApiKey || ''),
102-
moonshotApiKey: String(moonshotApiKey || '')
116+
useOwnKeys: (useOwnKeys && hasPermission) || hasAnyProxyKey || hasBedrockKey,
117+
anthropicApiKey: anthropic,
118+
mistralApiKey: mistral,
119+
openaiApiKey: openai,
120+
moonshotApiKey: moonshot,
121+
bedrockBearerToken: String(bedrockBearerToken || '')
103122
}
104123
} catch (error) {
105124
remixAILogger.warn('[ApiKeySettingsHelper] Failed to read user API keys config:', error)
@@ -112,6 +131,12 @@ export class ApiKeySettingsHelper {
112131
*/
113132
async isUsingOwnApiKeyForProvider(provider: string): Promise<boolean> {
114133
try {
134+
// Bedrock has no proxy — it's always "own key" when a Bedrock API key is
135+
// present, independent of the proxy-vs-own-key toggle.
136+
if (provider === 'bedrock') {
137+
return !!(await this.getSetting('deepagent-bedrock-bearer-token'))
138+
}
139+
115140
const useOwnKeysValue = await this.getSetting('deepagent-api-keys-config')
116141
const useOwnKeys = useOwnKeysValue === 'true' || useOwnKeysValue === true
117142

apps/remix-ide/src/app/plugins/remixAI/DeepAgentManager.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { remixAILogger, CONVERSATION_THREAD_PREFIX, DeepAgentInferencer, getBestAvailableModel } from '@remix/remix-ai-core'
1+
import { remixAILogger, CONVERSATION_THREAD_PREFIX, DeepAgentInferencer, getBestAvailableModel, isUsingOwnKeyForProvider } from '@remix/remix-ai-core'
22
import type { IRemixAIPlugin, ToolApprovalResponse } from './types'
33
import type { DeepAgentEventBridge } from './DeepAgentEventBridge'
44
import type { MCPServerManager } from './MCPServerManager'
@@ -88,8 +88,9 @@ export class DeepAgentManager {
8888
remixAILogger.log('[RemixAI Plugin] Using user-provided API keys for DeepAgent')
8989
}
9090
const resolvedModelId = await this.resolveOllamaModelId(plugin.selectedModel.provider, plugin.selectedModelId)
91-
// Don't use remote fallback for Ollama - user explicitly chose local models
92-
const fallbackInferencer = plugin.selectedModel.provider === 'ollama' ? null : plugin.remoteInferencer
91+
const fallbackInferencer = (plugin.selectedModel.provider === 'ollama' || isUsingOwnKeyForProvider(plugin.selectedModel.provider, userApiKeys))
92+
? null
93+
: plugin.remoteInferencer
9394

9495
// Clean up old instance if it exists
9596
if (plugin.deepAgentInferencer && typeof plugin.deepAgentInferencer.cleanup === 'function') {
@@ -107,7 +108,7 @@ export class DeepAgentManager {
107108
},
108109
fallbackInferencer,
109110
plugin.mcpInferencer,
110-
{ provider: plugin.selectedModel.provider as 'anthropic' | 'mistralai' | 'openai' | 'moonshot' | 'ollama', modelId: resolvedModelId }
111+
{ provider: plugin.selectedModel.provider as 'anthropic' | 'mistralai' | 'openai' | 'moonshot' | 'ollama' | 'bedrock', modelId: resolvedModelId }
111112
)
112113

113114
await plugin.deepAgentInferencer.initialize()
@@ -283,6 +284,7 @@ export class DeepAgentManager {
283284
}
284285

285286
private async doReinitialize(): Promise<void> {
287+
console.log('[DeepAgentManager] doReinitialize: starting reinitialization of DeepAgentInferencer')
286288
const plugin = this.deps.plugin
287289
const hasSelectedModel = !!(plugin.selectedModel && plugin.selectedModelId)
288290

@@ -308,8 +310,9 @@ export class DeepAgentManager {
308310
remixAILogger.log('[RemixAI Plugin] Using user-provided API keys for DeepAgent (reinitialize)')
309311
}
310312
const resolvedModelId = await this.resolveOllamaModelId(plugin.selectedModel.provider, plugin.selectedModelId)
311-
// Don't use remote fallback for Ollama - user explicitly chose local models
312-
const fallbackInferencer = plugin.selectedModel.provider === 'ollama' ? null : plugin.remoteInferencer
313+
const fallbackInferencer = (plugin.selectedModel.provider === 'ollama' || isUsingOwnKeyForProvider(plugin.selectedModel.provider, userApiKeys))
314+
? null
315+
: plugin.remoteInferencer
313316

314317
// Clean up old instance if it exists
315318
if (plugin.deepAgentInferencer && typeof plugin.deepAgentInferencer.cleanup === 'function') {
@@ -327,7 +330,7 @@ export class DeepAgentManager {
327330
},
328331
fallbackInferencer,
329332
plugin.mcpInferencer,
330-
{ provider: plugin.selectedModel.provider as 'anthropic' | 'mistralai' | 'openai' | 'moonshot' | 'ollama', modelId: resolvedModelId }
333+
{ provider: plugin.selectedModel.provider as 'anthropic' | 'mistralai' | 'openai' | 'moonshot' | 'ollama' | 'bedrock', modelId: resolvedModelId }
331334
)
332335
await plugin.deepAgentInferencer.initialize()
333336
plugin.deepAgentEnabled = true
@@ -357,6 +360,7 @@ export class DeepAgentManager {
357360
remixAILogger.log('[RemixAI Plugin] DeepAgent reinitialized successfully')
358361
} catch (error) {
359362
remixAILogger.error('[RemixAI Plugin] Failed to reinitialize DeepAgent:', error)
363+
console.error('[DeepAgentManager] doReinitialize: caught error', error)
360364
plugin.deepAgentEnabled = false
361365
plugin.deepAgentInferencer = null
362366
;(plugin as any).traceDeepAgentLifecycle?.('manager.reinitialize:failed', 'caught error inside DeepAgentManager.reinitialize()', {

apps/remix-ide/src/app/plugins/remixAIPlugin.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Plugin } from '@remixproject/engine';
33
import { trackMatomoEvent, Features, ChatPromptMetadata } from '@remix-api'
44
import { remixAILogger, RemoteInferencer, IRemoteModel, IParams, GenerationParams, AssistantParams, CodeExplainAgent, SecurityAgent, CompletionParams, OllamaInferencer } from '@remix/remix-ai-core';
55
import { CodeCompletionAgent, ContractAgent, workspaceAgent, IContextType, mcpDefaultServersConfig, mcpBasicServersConfig, mcpWebSearchServersConfig } from '@remix/remix-ai-core';
6-
import { MCPInferencer, DeepAgentInferencer, onApiKeysChange } from '@remix/remix-ai-core';
6+
import { MCPInferencer, DeepAgentInferencer, onApiKeysChange, isUsingOwnKeyForProvider } from '@remix/remix-ai-core';
77
import { IMCPServer, IMCPConnectionStatus } from '@remix/remix-ai-core';
88
import { RemixMCPServer, createRemixMCPServer } from '@remix/remix-ai-core';
99
import { AIModel } from '@remix/remix-ai-core';
@@ -493,8 +493,9 @@ export class RemixAIPlugin extends Plugin {
493493
remixAILogger.log('[RemixAI Plugin] Using user-provided API keys for DeepAgent')
494494
}
495495

496-
// Don't use remote fallback for Ollama - user explicitly chose local models
497-
const fallbackInferencer = this.selectedModel.provider === 'ollama' ? null : this.remoteInferencer
496+
const fallbackInferencer = (this.selectedModel.provider === 'ollama' || isUsingOwnKeyForProvider(this.selectedModel.provider, userApiKeys))
497+
? null
498+
: this.remoteInferencer
498499

499500
// Clean up old instance if it exists
500501
if (this.deepAgentInferencer && typeof this.deepAgentInferencer.cleanup === 'function') {
@@ -512,7 +513,7 @@ export class RemixAIPlugin extends Plugin {
512513
},
513514
fallbackInferencer,
514515
this.mcpInferencer, // Pass MCPInferencer to gather external MCP client tools
515-
{ provider: this.selectedModel.provider as 'anthropic' | 'mistralai' | 'openai' | 'moonshot' | 'ollama', modelId: this.selectedModelId } // Pass selected model
516+
{ provider: this.selectedModel.provider as 'anthropic' | 'mistralai' | 'openai' | 'moonshot' | 'ollama' | 'bedrock', modelId: this.selectedModelId } // Pass selected model
516517
)
517518
await this.deepAgentInferencer.initialize()
518519
// Set up DeepAgent event listeners for streaming (once only)
@@ -743,6 +744,7 @@ export class RemixAIPlugin extends Plugin {
743744
}
744745
}
745746
remixAILogger.log('[answer][route-flow]', routeFlow)
747+
console.log('[answer][route-flow] route', route)
746748
if (!remoteRouteCheck && route === 'remote') {
747749
remixAILogger.warn('[answer][route-flow] remote route selected but remoteInferencer is missing')
748750
}

apps/remix-ide/src/app/tabs/locales/en/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"settings.deepagent-mistral-api-key": "MistralAI API Key",
108108
"settings.deepagent-openai-api-key": "OpenAI API Key",
109109
"settings.deepagent-moonshot-api-key": "Moonshot/Kimi API Key",
110+
"settings.deepagent-bedrock-bearer-token": "AWS Bedrock API Key",
110111
"settings.testApiKey": "Test",
111112
"settings.testing": "Testing...",
112113
"settings.apiKeyValid": "API key is valid",

apps/remix-ide/webpack.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
123123
module: false,
124124
tls: false,
125125
net: false,
126+
http2: false,
127+
dns: false,
126128
readline: false,
127129
child_process: false,
128130
buffer: require.resolve('buffer/'),
@@ -155,6 +157,9 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
155157
// Prefer browser/Esm entry points where available
156158
config.resolve.mainFields = ['browser', 'module', 'main']
157159

160+
// Honor the `browser` field remaps in package.json (object form) for the AWS SDK
161+
config.resolve.aliasFields = ['browser']
162+
158163
config.resolve.alias = {
159164
...config.resolve.alias,
160165
// Avoid bundling server-only deps or optional node paths
@@ -276,6 +281,8 @@ module.exports = composePlugins(withNx(), withReact(), (config) => {
276281
`)
277282
} else if (replacements[module]) {
278283
resource.request = replacements[module]
284+
} else {
285+
resource.request = module
279286
}
280287
})
281288
)

libs/remix-ai-core/src/helpers/apiKeyValidator.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ export function validateApiKeyFormat(provider: ModelProvider, apiKey: string): A
7979
}
8080
break
8181

82+
case 'bedrock':
83+
if (trimmedKey.length < 20) {
84+
return {
85+
isValid: false,
86+
provider,
87+
error: 'AWS Bedrock API key appears to be too short'
88+
}
89+
}
90+
break
91+
8292
case 'ollama':
8393
return {
8494
isValid: true,
@@ -117,6 +127,9 @@ export async function testApiKey(provider: ModelProvider, apiKey: string): Promi
117127
case 'moonshot':
118128
return await testMoonshotKey(trimmedKey)
119129

130+
case 'bedrock':
131+
return await testBedrockKey(trimmedKey)
132+
120133
case 'ollama':
121134
return { isValid: true, provider }
122135

@@ -311,7 +324,63 @@ async function testMoonshotKey(apiKey: string): Promise<ApiKeyValidationResult>
311324
}
312325
}
313326

327+
async function testBedrockKey(apiKey: string): Promise<ApiKeyValidationResult> {
328+
const region = 'us-east-1'
329+
const modelId = 'amazon.nova-micro-v1:0'
330+
try {
331+
const response = await fetch(
332+
`https://bedrock-runtime.${region}.amazonaws.com/model/${encodeURIComponent(modelId)}/converse`,
333+
{
334+
method: 'POST',
335+
headers: {
336+
'Content-Type': 'application/json',
337+
'Authorization': `Bearer ${apiKey}`
338+
},
339+
body: JSON.stringify({
340+
messages: [{ role: 'user', content: [{ text: 'ping' }]}],
341+
inferenceConfig: { maxTokens: 1 }
342+
})
343+
}
344+
)
345+
346+
// Authenticated and served.
347+
if (response.ok) {
348+
return { isValid: true, provider: 'bedrock' }
349+
}
350+
351+
// Bad / expired / unrecognized token.
352+
if (response.status === 401 || response.status === 403) {
353+
const errorData = await response.json().catch(() => ({} as any))
354+
return {
355+
isValid: false,
356+
provider: 'bedrock',
357+
error: errorData?.message || errorData?.Message || 'Invalid API key - authentication failed'
358+
}
359+
}
360+
361+
// Throttled, or a request-shape validation error — the token still
362+
// authenticated (an invalid one is rejected with 401/403 first).
363+
if (response.status === 429 || response.status === 400) {
364+
return { isValid: true, provider: 'bedrock' }
365+
}
366+
367+
const errorData = await response.json().catch(() => ({} as any))
368+
return {
369+
isValid: false,
370+
provider: 'bedrock',
371+
error: errorData?.message || errorData?.Message || `API returned status ${response.status}`
372+
}
373+
} catch (error: any) {
374+
return {
375+
isValid: false,
376+
provider: 'bedrock',
377+
error: error?.message || 'Network error testing API key'
378+
}
379+
}
380+
}
381+
314382
export function getProviderFromSettingKey(settingKey: string): ModelProvider | null {
383+
if (settingKey.includes('bedrock')) return 'bedrock'
315384
if (settingKey.includes('anthropic')) return 'anthropic'
316385
if (settingKey.includes('openai')) return 'openai'
317386
if (settingKey.includes('mistral')) return 'mistralai'

libs/remix-ai-core/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { ICompletions,
44
IParams, ChatEntry, AIRequestType, IRemoteModel } from './types/types'
55
import { ModelType } from './types/constants'
6-
import { InsertionParams, CompletionParams, GenerationParams, AssistantParams, AIModel, ANONYMOUS_FALLBACK_MODELS, ANONYMOUS_PLACEHOLDER_MODEL, OLLAMA_MODEL, getModelById, parseAIModelsFromPermissions } from './types/models'
6+
import { InsertionParams, CompletionParams, GenerationParams, AssistantParams, AIModel, ANONYMOUS_FALLBACK_MODELS, ANONYMOUS_PLACEHOLDER_MODEL, OLLAMA_MODEL, BEDROCK_MODELS, getModelById, parseAIModelsFromPermissions } from './types/models'
77
import { buildChatPrompt } from './prompts/promptBuilder'
88
import { RemoteInferencer } from './inferencers/remote/remoteInference'
99
import { OllamaInferencer } from './inferencers/local/ollamaInferencer'
@@ -28,7 +28,7 @@ export {
2828
InsertionParams, CompletionParams, GenerationParams, AssistantParams,
2929
ChatEntry, AIRequestType, ChatHistory, resetOllamaHostOnSettingsChange,
3030
mcpDefaultServersConfig, mcpBasicServersConfig, mcpWebSearchServersConfig,
31-
AIModel, ANONYMOUS_FALLBACK_MODELS, ANONYMOUS_PLACEHOLDER_MODEL, OLLAMA_MODEL, getModelById, parseAIModelsFromPermissions,
31+
AIModel, ANONYMOUS_FALLBACK_MODELS, ANONYMOUS_PLACEHOLDER_MODEL, OLLAMA_MODEL, BEDROCK_MODELS, getModelById, parseAIModelsFromPermissions,
3232
ChatHistoryStorageManager, IndexedDBChatHistoryBackend,
3333
WeightedToolSelector, IChatMessage,
3434
remixAILogger, setRemixAILoggingEnabled, isRemixAILoggingEnabled

libs/remix-ai-core/src/inferencers/deepagent/DeepAgentInferencer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,6 @@ export class DeepAgentInferencer implements ICompletions, IGeneration {
799799
}
800800

801801
public async getProjectStructure(): Promise<string> {
802-
console.log('[DeepAgentInferencer] Attempting to retrieve project structure from MCP...')
803802
if (!this.mcpInferencer) {
804803
return ''
805804
}

0 commit comments

Comments
 (0)