LLM Multi-Provider Example
Demonstrates how to use multiple LLM providers (Claude and OpenAI) in the same workflow. The same prompt is sent to both providers and the responses are displayed side by side.
Key Concepts
One tool, multiple providers via call-time config
The same tool class (LlmGenerateTextTool) is injected once via the constructor. Each call passes the desired provider and model via { config: { ... } }:
constructor(private readonly llmGenerateText: LlmGenerateTextTool) {
super();
}
// Claude call
const result = await this.llmGenerateText.call(
{ prompt: args.prompt },
{
config: {
provider: 'claude',
model: 'claude-sonnet-4-6',
system: 'You are a helpful assistant. Keep your response brief.',
},
},
);
// OpenAI call
const result = await this.llmGenerateText.call(
{ prompt: state.prompt },
{
config: {
provider: 'openai',
model: 'gpt-4o-mini',
system: 'You are a helpful assistant. Keep your response brief.',
},
},
);Module setup
Import both provider modules. The adapter tools are available globally via LlmProviderModule:
@Module({
imports: [LoopCoreModule, ClaudeModule, OpenAiModule],
providers: [LlmMultiProviderWorkflow],
})
export class LlmMultiProviderExampleModule {}App registration
Register the module in your app with @StudioApp so workflows appear in Studio:
import { StudioApp } from '@loopstack/common';
import {
LlmMultiProviderExampleModule,
LlmMultiProviderWorkflow,
} from '@loopstack/llm-multi-provider-example-workflow';
@StudioApp({
title: 'Multi-Provider Example',
workflows: [LlmMultiProviderWorkflow],
})
@Module({
imports: [LlmMultiProviderExampleModule],
})
export class MyAppModule {}Full Workflow
import { z } from 'zod';
import { BaseWorkflow, Transition, Workflow } from '@loopstack/common';
import type { RunContext } from '@loopstack/common';
import { LlmGenerateTextTool, LlmMessageDocument } from '@loopstack/llm-provider-module';
interface LlmMultiProviderState {
prompt: string;
}
@Workflow({
title: 'LLM Multi-Provider',
description: 'Runs the same prompt through Claude and OpenAI side by side',
widget: __dirname + '/llm-multi-provider.ui.yaml',
schema: z.object({
prompt: z.string().default('What is the meaning of life? Answer in one sentence.'),
}),
})
export class LlmMultiProviderWorkflow extends BaseWorkflow<{ prompt: string }, LlmMultiProviderState> {
constructor(private readonly llmGenerateText: LlmGenerateTextTool) {
super();
}
@Transition({ to: 'claude_done' })
async askClaude(state: LlmMultiProviderState, ctx: RunContext): Promise<LlmMultiProviderState> {
const args = ctx.args as { prompt: string };
await this.documentStore.save(LlmMessageDocument, { role: 'user', text: args.prompt });
const result = await this.llmGenerateText.call(
{ prompt: args.prompt },
{
config: {
provider: 'claude',
model: 'claude-sonnet-4-6',
system: 'You are a helpful assistant. Keep your response brief.',
},
},
);
await this.documentStore.save(LlmMessageDocument, {
role: 'assistant',
text: `**Claude:** ${result.data!.message.text}`,
});
return { ...state, prompt: args.prompt };
}
@Transition({ from: 'claude_done', to: 'openai_done' })
async askOpenAi(state: LlmMultiProviderState): Promise<LlmMultiProviderState> {
const result = await this.llmGenerateText.call(
{ prompt: state.prompt },
{
config: {
provider: 'openai',
model: 'gpt-4o-mini',
system: 'You are a helpful assistant. Keep your response brief.',
},
},
);
await this.documentStore.save(LlmMessageDocument, {
role: 'assistant',
text: `**OpenAI:** ${result.data!.message.text}`,
});
return state;
}
@Transition({ from: 'openai_done', to: 'end' })
async done(_state: LlmMultiProviderState): Promise<unknown> {
return {};
}
}How It Works
- User provides a prompt (defaults to “What is the meaning of life?”)
- The prompt is sent to Claude via
this.llmGenerateText.call()withconfig: { provider: 'claude' } - Claude’s response is saved as a document
- The same prompt is sent to OpenAI via
this.llmGenerateText.call()withconfig: { provider: 'openai' } - OpenAI’s response is saved as a document
- Both responses are visible in the UI
Best Practices Demonstrated
- Same class, different config — one
LlmGenerateTextToolinstance, differentconfigper call - Provider-agnostic tool — the tool class works with any registered provider
- Explicit provider declaration —
provider: 'claude'/provider: 'openai'in config makes it clear which provider is used
Dependencies
@loopstack/llm-provider-module— Adapter tools and shared types@loopstack/claude-module— Claude LLM provider (registers as'claude')@loopstack/openai-module— OpenAI LLM provider (registers as'openai')
Environment Variables
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY | Anthropic API key for Claude |
OPENAI_API_KEY | OpenAI API key |
Last updated on