/** * Model factory — hybrid Strands-native + Vercel AI SDK bridge. * * Native Strands models are used where they work cleanly in the browser: * - anthropic → AnthropicModel (dangerouslyAllowBrowser) * - openai → OpenAIModel (dangerouslyAllowBrowser) * - google → GoogleModel (browser-friendly by default) * * Everything else — including Bedrock with bearer-token auth — goes * through VercelModel wrapping the corresponding @ai-sdk/. * This unlocks ~8 extra providers without writing any glue: * - bedrock → @ai-sdk/amazon-bedrock (bearer token, no SigV4) * - openrouter → @openrouter/ai-sdk-provider (300+ models behind 1 key) * - groq, mistral, deepseek, cohere, perplexity, xai → their @ai-sdk/* pkgs * - webllm → VercelModel + @browser-ai/web-llm (on-device) * * Why hybrid? Native models have slightly richer event shapes (reasoning, * cache deltas, citations) that don't round-trip through LanguageModelV3 * cleanly yet. For the providers where that matters (Anthropic / OpenAI / * Google direct) we stay native; for the rest VercelModel is excellent. * * See: * - https://github.com/strands-agents/sdk-typescript/tree/main/packages/sdk/src/models * - https://sdk.vercel.ai/providers/ai-sdk-providers */ import type { Settings } from '../types/index' export async function createModel(settings: Settings): Promise { const { provider, model, apiKey } = settings const maxTokens = settings.maxTokens const params: Record = {} if (typeof settings.temperature === 'number') params.temperature = settings.temperature if (typeof settings.topP === 'number') params.top_p = settings.topP if (settings.stopSequences?.length) params.stop_sequences = settings.stopSequences switch (provider) { /* ── Native Strands models (richest event shapes) ─────────────── */ case 'anthropic': { const { AnthropicModel } = await import('@strands-agents/sdk/models/anthropic') return new AnthropicModel({ apiKey, modelId: model || 'claude-opus-4-7', ...(typeof maxTokens === 'number' ? { maxTokens } : {}), ...(Object.keys(params).length ? { params } : {}), clientConfig: { dangerouslyAllowBrowser: true }, }) } case 'openai': { const { OpenAIModel } = await import('@strands-agents/sdk/models/openai') return new OpenAIModel({ api: 'chat', apiKey, modelId: model || 'gpt-5.5-2026-04-23', ...(typeof maxTokens === 'number' ? { maxTokens } : {}), ...(Object.keys(params).length ? { params } : {}), clientConfig: { dangerouslyAllowBrowser: true }, } as any) } case 'google': { const { GoogleModel } = await import('@strands-agents/sdk/models/google') return new GoogleModel({ apiKey, modelId: model || 'gemini-2.5-flash', ...(typeof maxTokens === 'number' ? { maxTokens } : {}), ...(Object.keys(params).length ? { params } : {}), }) } /* ── Vercel AI SDK bridges ────────────────────────────────────── */ case 'bedrock': { // @ai-sdk/amazon-bedrock has first-class bearer-token auth via apiKey, // bypassing SigV4 entirely (browser-safe). // https://sdk.vercel.ai/providers/ai-sdk-providers/amazon-bedrock if (!apiKey) throw new Error('Bedrock API key is missing. Add it in Settings → Model.') const [{ createAmazonBedrock }, { VercelModel }] = await Promise.all([ import('@ai-sdk/amazon-bedrock'), import('@strands-agents/sdk/models/vercel'), ]) const bedrock = createAmazonBedrock({ apiKey, region: settings.bedrockRegion || 'us-west-2', }) const lm = bedrock(model || 'global.anthropic.claude-opus-4-7') return new VercelModel({ provider: lm, ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), ...(typeof settings.topP === 'number' ? { topP: settings.topP } : {}), ...(settings.stopSequences?.length ? { stopSequences: settings.stopSequences } : {}), } as any) } case 'openrouter': { // 300+ models (Claude, GPT, Llama, Mixtral, Qwen, DeepSeek, etc.) through 1 key. // https://openrouter.ai/docs if (!apiKey) throw new Error('OpenRouter API key is missing.') const [{ createOpenRouter }, { VercelModel }] = await Promise.all([ import('@openrouter/ai-sdk-provider'), import('@strands-agents/sdk/models/vercel'), ]) const openrouter = createOpenRouter({ apiKey }) const lm = openrouter(model || 'anthropic/claude-opus-4.7') return new VercelModel({ provider: lm, ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), ...(typeof settings.topP === 'number' ? { topP: settings.topP } : {}), } as any) } case 'groq': { if (!apiKey) throw new Error('Groq API key is missing.') const [{ createGroq }, { VercelModel }] = await Promise.all([ import('@ai-sdk/groq'), import('@strands-agents/sdk/models/vercel'), ]) const groq = createGroq({ apiKey }) return new VercelModel({ provider: groq(model || 'llama-3.3-70b-versatile'), ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), } as any) } case 'mistral': { if (!apiKey) throw new Error('Mistral API key is missing.') const [{ createMistral }, { VercelModel }] = await Promise.all([ import('@ai-sdk/mistral'), import('@strands-agents/sdk/models/vercel'), ]) const mistral = createMistral({ apiKey }) return new VercelModel({ provider: mistral(model || 'mistral-large-latest'), ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), } as any) } case 'deepseek': { if (!apiKey) throw new Error('DeepSeek API key is missing.') const [{ createDeepSeek }, { VercelModel }] = await Promise.all([ import('@ai-sdk/deepseek'), import('@strands-agents/sdk/models/vercel'), ]) const deepseek = createDeepSeek({ apiKey }) return new VercelModel({ provider: deepseek(model || 'deepseek-chat'), ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), } as any) } case 'cohere': { if (!apiKey) throw new Error('Cohere API key is missing.') const [{ createCohere }, { VercelModel }] = await Promise.all([ import('@ai-sdk/cohere'), import('@strands-agents/sdk/models/vercel'), ]) const cohere = createCohere({ apiKey }) return new VercelModel({ provider: cohere(model || 'command-r-plus'), ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), } as any) } case 'perplexity': { if (!apiKey) throw new Error('Perplexity API key is missing.') const [{ createPerplexity }, { VercelModel }] = await Promise.all([ import('@ai-sdk/perplexity'), import('@strands-agents/sdk/models/vercel'), ]) const pplx = createPerplexity({ apiKey }) return new VercelModel({ provider: pplx(model || 'sonar-pro'), ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), } as any) } case 'xai': { if (!apiKey) throw new Error('xAI API key is missing.') const [{ createXai }, { VercelModel }] = await Promise.all([ import('@ai-sdk/xai'), import('@strands-agents/sdk/models/vercel'), ]) const xai = createXai({ apiKey }) return new VercelModel({ provider: xai(model || 'grok-2-latest'), ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), ...(typeof settings.temperature === 'number' ? { temperature: settings.temperature } : {}), } as any) } case 'webllm': { // 100% local via WebGPU const [{ VercelModel }, { createWebLLM, doesBrowserSupportWebLLM }] = await Promise.all([ import('@strands-agents/sdk/models/vercel'), import('@browser-ai/web-llm'), ]) if (!doesBrowserSupportWebLLM()) { throw new Error('WebLLM requires WebGPU. Try Chrome/Edge on desktop.') } const client = createWebLLM() const lm = client(model || 'Qwen2.5-0.5B-Instruct-q4f16_1-MLC') return new VercelModel({ provider: lm, ...(typeof maxTokens === 'number' ? { maxOutputTokens: maxTokens } : {}), } as any) } default: { // Fall back to anthropic — old behavior const { AnthropicModel } = await import('@strands-agents/sdk/models/anthropic') return new AnthropicModel({ apiKey, modelId: model || 'claude-opus-4-7', clientConfig: { dangerouslyAllowBrowser: true }, }) } } }