Skip to content

Commit c127a42

Browse files
committed
thighthening
1 parent aac6b3e commit c127a42

16 files changed

Lines changed: 258 additions & 160 deletions

File tree

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ export class ApiKeySettingsHelper {
6666
const hasPermission = await this.canUseOwnApiKeys()
6767

6868
// 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.
69+
// Bedrock API key regardless of `hasPermission`: a present key means the
70+
// user wants direct access, which must be honoured. When absent, Bedrock
71+
// falls back to the Remix proxy (handled in the ModelFactory). The
72+
// permission flag only governs own-key access on the proxy-backed
73+
// providers below.
7374
const [
7475
useOwnKeysValue,
7576
anthropicApiKey,
@@ -84,7 +85,7 @@ export class ApiKeySettingsHelper {
8485
this.getSetting('deepagent-mistral-api-key'),
8586
this.getSetting('deepagent-openai-api-key'),
8687
this.getSetting('deepagent-moonshot-api-key'),
87-
this.getSetting('deepagent-openrouter-api-key')
88+
this.getSetting('deepagent-openrouter-api-key'),
8889
this.getSetting('deepagent-bedrock-bearer-token')
8990
])
9091

@@ -124,7 +125,7 @@ export class ApiKeySettingsHelper {
124125
mistralApiKey: String(mistralApiKey || ''),
125126
openaiApiKey: String(openaiApiKey || ''),
126127
moonshotApiKey: String(moonshotApiKey || ''),
127-
openrouterApiKey: String(openrouterApiKey || '')
128+
openrouterApiKey: String(openrouterApiKey || ''),
128129
bedrockBearerToken: String(bedrockBearerToken || '')
129130
}
130131
} catch (error) {
@@ -138,8 +139,8 @@ export class ApiKeySettingsHelper {
138139
*/
139140
async isUsingOwnApiKeyForProvider(provider: string): Promise<boolean> {
140141
try {
141-
// Bedrock has no proxy — it's always "own key" when a Bedrock API key is
142-
// present, independent of the proxy-vs-own-key toggle.
142+
// A present Bedrock API key means direct ("own key") access, independent
143+
// of the proxy-vs-own-key toggle; without one we route through the proxy.
143144
if (provider === 'bedrock') {
144145
return !!(await this.getSetting('deepagent-bedrock-bearer-token'))
145146
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { PermissionChecker } from './PermissionChecker'
66
export interface MCPServerManagerDeps {
77
plugin: IRemixAIPlugin
88
permissionChecker: PermissionChecker
9-
setModel: (modelId: string) => Promise<void>
9+
setModel: (modelId: string, provider?: string) => Promise<void>
1010
reinitializeDeepAgent: () => Promise<void>
1111
}
1212

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { remixAILogger,
77
getBestAvailableModel,
88
listModels,
99
modelSupportsTools,
10-
getModelById
10+
getModelById,
11+
findModel,
12+
ANONYMOUS_FALLBACK_MODELS
1113
} from '@remix/remix-ai-core'
1214
import type { AIModel } from '@remix/remix-ai-core'
1315
import type { IRemixAIPlugin } from './types'
@@ -26,23 +28,18 @@ export class ModelManager {
2628
this.deps = deps
2729
}
2830

29-
async setModel(modelId: string, allowedModels: string[] = []): Promise<void> {
31+
async setModel(modelId: string, allowedModels: string[] = [], provider?: string): Promise<void> {
3032
const plugin = this.deps.plugin
31-
// The static `getModelById` only knows the anonymous fallback list
32-
// (placeholder + ollama). Real model metadata lives in the
33-
// assistantState plugin, fed by /permissions.ai_models. Look it up
34-
// there first; only fall back to the static helper for the bootstrap
35-
// / ollama cases.
3633
let model: AIModel | undefined
3734
try {
3835
const dynamic: AIModel[] = await plugin.call('assistantState', 'getAvailableModels')
3936
if (Array.isArray(dynamic)) {
40-
model = dynamic.find(m => m.id === modelId)
37+
model = findModel(dynamic, modelId, provider)
4138
}
4239
} catch (e) {
4340
remixAILogger.warn('[ModelManager] assistantState.getAvailableModels failed', e)
4441
}
45-
if (!model) model = getModelById(modelId)
42+
if (!model) model = findModel(ANONYMOUS_FALLBACK_MODELS, modelId, provider) ?? getModelById(modelId)
4643
if (!model) {
4744
// No silent fallback. The picker is fed by /permissions — if a
4845
// caller asks for a model id that isn't in any catalogue we have a
@@ -180,7 +177,7 @@ export class ModelManager {
180177
throw new Error(`[ModelManager.setAssistantProvider] No available model for provider "${provider}" in /permissions ai_models. Backend must advertise at least one row for this provider.`)
181178
}
182179
const chosen = candidates.find(m => m.isDefault) ?? candidates[0]
183-
await this.setModel(chosen.id)
180+
await this.setModel(chosen.id, [], chosen.provider)
184181
}
185182

186183
async getOllamaModels(): Promise<{ name: string; supported: boolean }[]> {

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export class RemixAIPlugin extends Plugin {
131131
this.mcpManager.setDeps({
132132
plugin: this as any,
133133
permissionChecker: this.permissionChecker,
134-
setModel: (modelId: string) => this.modelManager.setModel(modelId),
134+
setModel: (modelId: string, provider?: string) => this.modelManager.setModel(modelId, [], provider),
135135
reinitializeDeepAgent: () => this.deepAgentManager.reinitialize()
136136
})
137137

@@ -357,7 +357,7 @@ export class RemixAIPlugin extends Plugin {
357357
// GenerationParams/CompletionParams pick up the provider+model
358358
// and DeepAgent (if enabled) reinitialises.
359359
try {
360-
await this.setModel(def.id)
360+
await this.setModel(def.id, def.provider)
361361
} catch (e) {
362362
remixAILogger.warn('[RemixAI Plugin] setModel failed during initial /permissions resolution', e)
363363
}
@@ -562,7 +562,7 @@ export class RemixAIPlugin extends Plugin {
562562
// resolved one. Without an id the picker is empty and downstream
563563
// setModel would throw — we let the assistantState subscription do it.
564564
if (this.selectedModelId) {
565-
await this.setModel(this.selectedModelId)
565+
await this.setModel(this.selectedModelId, this.selectedModel?.provider)
566566
} else {
567567
remixAILogger.log('[RemixAI Plugin] initialize: no selectedModelId yet, deferring setModel until /permissions loads')
568568
}
@@ -744,15 +744,10 @@ export class RemixAIPlugin extends Plugin {
744744
}
745745
}
746746
remixAILogger.log('[answer][route-flow]', routeFlow)
747-
console.log('[answer][route-flow] route', route)
748747
if (!remoteRouteCheck && route === 'remote') {
749748
remixAILogger.warn('[answer][route-flow] remote route selected but remoteInferencer is missing')
750749
}
751750
if (route === 'deepagent') {
752-
// If a previous cancelRequest is still rebuilding the inferencer,
753-
// wait for it to finish so this dispatch lands on the new
754-
// instance with a clean LangGraph pipe rather than racing the
755-
// about-to-be-discarded one.
756751
await this.deepAgentManager.awaitReady()
757752
remixAILogger.log('[answer][route-flow] dispatch=deepagent.answer')
758753
return await this.deepAgentInferencer.answer(newPrompt, params, this.workspaceAgent.ctxFiles || '')
@@ -1014,8 +1009,8 @@ export class RemixAIPlugin extends Plugin {
10141009
return this.modelManager.setAssistantProvider(provider)
10151010
}
10161011

1017-
async setModel(modelId: string, allowedModels: string[] = []) {
1018-
return this.modelManager.setModel(modelId, allowedModels)
1012+
async setModel(modelId: string, provider?: string, allowedModels: string[] = []) {
1013+
return this.modelManager.setModel(modelId, allowedModels, provider)
10191014
}
10201015

10211016
async setOllamaModel(ollamaModelName: string) {

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

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export function validateApiKeyFormat(provider: ModelProvider, apiKey: string): A
8787
error: 'OpenRouter API key should start with "sk-or-"'
8888
}
8989
}
90+
break
91+
9092
case 'bedrock':
9193
if (trimmedKey.length < 20) {
9294
return {
@@ -137,7 +139,7 @@ export async function testApiKey(provider: ModelProvider, apiKey: string): Promi
137139

138140
case 'openrouter':
139141
return await testOpenRouterKey(trimmedKey)
140-
142+
141143
case 'bedrock':
142144
return await testBedrockKey(trimmedKey)
143145

@@ -335,6 +337,47 @@ async function testMoonshotKey(apiKey: string): Promise<ApiKeyValidationResult>
335337
}
336338
}
337339

340+
async function testOpenRouterKey(apiKey: string): Promise<ApiKeyValidationResult> {
341+
try {
342+
const response = await fetch('https://openrouter.ai/api/v1/key', {
343+
method: 'GET',
344+
headers: {
345+
'Authorization': `Bearer ${apiKey}`
346+
}
347+
})
348+
349+
if (response.ok) {
350+
return { isValid: true, provider: 'openrouter' }
351+
}
352+
353+
if (response.status === 401) {
354+
return {
355+
isValid: false,
356+
provider: 'openrouter',
357+
error: 'Invalid API key - authentication failed'
358+
}
359+
}
360+
361+
if (response.status === 429) {
362+
// Rate limited but key is valid
363+
return { isValid: true, provider: 'openrouter' }
364+
}
365+
366+
const errorData = await response.json().catch(() => ({}))
367+
return {
368+
isValid: false,
369+
provider: 'openrouter',
370+
error: errorData?.error?.message || `API returned status ${response.status}`
371+
}
372+
} catch (error: any) {
373+
return {
374+
isValid: false,
375+
provider: 'openrouter',
376+
error: error?.message || 'Network error testing API key'
377+
}
378+
}
379+
}
380+
338381
async function testBedrockKey(apiKey: string): Promise<ApiKeyValidationResult> {
339382
const region = 'us-east-1'
340383
const modelId = 'amazon.nova-micro-v1:0'
@@ -369,8 +412,6 @@ async function testBedrockKey(apiKey: string): Promise<ApiKeyValidationResult> {
369412
}
370413
}
371414

372-
// Throttled, or a request-shape validation error — the token still
373-
// authenticated (an invalid one is rejected with 401/403 first).
374415
if (response.status === 429 || response.status === 400) {
375416
return { isValid: true, provider: 'bedrock' }
376417
}

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, BEDROCK_MODELS, getModelById, parseAIModelsFromPermissions } from './types/models'
6+
import { InsertionParams, CompletionParams, GenerationParams, AssistantParams, AIModel, ANONYMOUS_FALLBACK_MODELS, ANONYMOUS_PLACEHOLDER_MODEL, OLLAMA_MODEL, BEDROCK_MODELS, getModelById, parseAIModelsFromPermissions, modelKey, parseModelKey, findModel } 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, BEDROCK_MODELS, getModelById, parseAIModelsFromPermissions,
31+
AIModel, ANONYMOUS_FALLBACK_MODELS, ANONYMOUS_PLACEHOLDER_MODEL, OLLAMA_MODEL, BEDROCK_MODELS, getModelById, parseAIModelsFromPermissions, modelKey, parseModelKey, findModel,
3232
ChatHistoryStorageManager, IndexedDBChatHistoryBackend,
3333
WeightedToolSelector, IChatMessage,
3434
remixAILogger, setRemixAILoggingEnabled, isRemixAILoggingEnabled

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

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { remixAILogger } from '../../helpers/logger'
22
import { ChatAnthropic } from '@langchain/anthropic'
33
import { ChatMistralAI } from '@langchain/mistralai'
44
import { ChatOpenAI } from '@langchain/openai'
5+
import { ChatOpenRouter } from '@langchain/openrouter'
56
import { ChatOllama } from '@langchain/ollama'
67
import { ChatBedrockConverse } from '@langchain/aws'
78
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
89
import { HTTPClient } from '@mistralai/mistralai/lib/http.js'
910
import { endpointUrls } from '@remix-endpoints-helper'
1011
import { ModelSelection, IUserApiKeyConfig } from '../../types/deepagent'
1112
import { DAPP_MAX_TOKENS } from './constants'
12-
import { getRemixAuthHeader } from '../auth'
13+
import { getRemixAuthHeader, getRemixAccessToken } from '../auth'
1314
import { discoverOllamaHost, getBestAvailableModel, getModelCapabilities } from '../local/ollama'
1415

1516
const AI_DEBUG = (() => {
@@ -460,39 +461,58 @@ export async function createModelInstance(
460461
case 'openrouter': {
461462
const useDirectApi = !!(userApiKeys?.useOwnKeys && userApiKeys?.openrouterApiKey)
462463
remixAILogger.log(`[ModelFactory] Creating OpenRouter model: ${modelId}${useDirectApi ? ' (direct API)' : ' (proxy)'}`)
464+
// Own key → talk to OpenRouter directly through its dedicated LangChain SDK.
465+
if (useDirectApi) {
466+
return wrapModelForDebug(new ChatOpenRouter({
467+
apiKey: userApiKeys!.openrouterApiKey as string,
468+
model: modelId,
469+
temperature: 0.7,
470+
maxTokens: maxTokens,
471+
maxRetries: 0,
472+
}), `openrouter/${modelId}`)
473+
}
474+
// No key → route through the Remix proxy (OpenAI-compatible endpoint).
463475
return wrapModelForDebug(new ChatOpenAI({
464-
apiKey: useDirectApi ? (userApiKeys!.openrouterApiKey as string) : 'proxy-handled',
476+
apiKey: 'proxy-handled',
465477
model: modelId,
466478
temperature: 0.7,
467479
maxTokens: maxTokens,
468480
streaming: true,
469481
maxRetries: 0,
470-
...(useDirectApi
471-
? {
472-
configuration: {
473-
baseURL: 'https://openrouter.ai/api/v1'
474-
}
475-
}
476-
: {
477-
configuration: {
478-
baseURL: `${endpointUrls.langchain}/openrouter`,
479-
fetch: authedFetch
480-
}
481-
})
482+
configuration: {
483+
baseURL: `${endpointUrls.langchain}/openrouter`,
484+
fetch: authedFetch
485+
}
482486
}), `openrouter/${modelId}`)
487+
}
488+
483489
case 'bedrock': {
484490
const bedrockBearerToken = userApiKeys?.bedrockBearerToken?.trim()
485-
if (!bedrockBearerToken) {
486-
throw new Error('[ModelFactory] AWS Bedrock models requires a Bedrock API key. Add it under Settings → Bring Your Own API Keys.')
487-
}
488-
489491
const region = DEFAULT_BEDROCK_REGION
490492
const bedrockModelId = resolveBedrockModelId(modelId, region)
491-
remixAILogger.log(`[ModelFactory] Creating AWS Bedrock model: ${bedrockModelId} @ ${region}`)
493+
const useDirectApi = !!bedrockBearerToken
494+
495+
if (useDirectApi) {
496+
remixAILogger.log(`[ModelFactory] Creating AWS Bedrock model: ${bedrockModelId} @ ${region} (direct API)`)
497+
return wrapModelForDebug(patchBedrockBindTools(new ChatBedrockConverse({
498+
model: bedrockModelId,
499+
region,
500+
bedrockBearerToken,
501+
})), `bedrock/${bedrockModelId}`)
502+
}
503+
504+
// Proxy mode: no user-provided key. Route the Bedrock Converse calls through
505+
const remixToken = getRemixAccessToken()
506+
if (!remixToken) {
507+
throw new Error('[ModelFactory] AWS Bedrock requires you to be signed in to use the Remix proxy, or add your own Bedrock API key under Settings → Bring Your Own API Keys.')
508+
}
509+
const proxyEndpointHost = `${endpointUrls.langchain}/bedrock`.replace(/^https?:\/\//, '')
510+
remixAILogger.log(`[ModelFactory] Creating AWS Bedrock model: ${bedrockModelId} @ ${region} (proxy)`)
492511
return wrapModelForDebug(patchBedrockBindTools(new ChatBedrockConverse({
493512
model: bedrockModelId,
494513
region,
495-
bedrockBearerToken,
514+
bedrockBearerToken: remixToken,
515+
endpointHost: proxyEndpointHost,
496516
})), `bedrock/${bedrockModelId}`)
497517
}
498518

libs/remix-ai-core/src/types/deepagent.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ export interface IUserApiKeyConfig {
1515
openaiApiKey?: string
1616
moonshotApiKey?: string
1717
openrouterApiKey?: string
18-
// AWS Bedrock API key (bearer token) for the `bedrock` provider. Bedrock has
19-
// no Remix proxy, so this is always user-provided. The key is region-scoped
20-
// at creation (see DEFAULT_BEDROCK_REGION in ModelFactory).
2118
bedrockBearerToken?: string
2219
}
2320

@@ -28,7 +25,6 @@ export function isUsingOwnKeyForProvider(
2825
if (!keys) return false
2926
switch (provider) {
3027
case 'bedrock':
31-
// Bedrock has no proxy — a configured key means own-key, always.
3228
return !!keys.bedrockBearerToken
3329
case 'anthropic':
3430
return !!(keys.useOwnKeys && keys.anthropicApiKey)
@@ -38,6 +34,8 @@ export function isUsingOwnKeyForProvider(
3834
return !!(keys.useOwnKeys && keys.openaiApiKey)
3935
case 'moonshot':
4036
return !!(keys.useOwnKeys && keys.moonshotApiKey)
37+
case 'openrouter':
38+
return !!(keys.useOwnKeys && keys.openrouterApiKey)
4139
default:
4240
return false
4341
}

0 commit comments

Comments
 (0)