Skip to content

Dynamic Models

This tutorial shows how to switch models mid-conversation based on context, complexity, or cost.

The Power of Dynamic Selection

In AIDK, model selection happens in render(). Since render runs on every tick, you can change models at any point:

tsx
class AdaptiveAgent extends Component {
  // Accumulate timeline entries across ticks
  private timeline = comState<COMTimelineEntry[]>("timeline", []);

  onTickStart(com, state) {
    if (state.current?.timeline) {
      this.timeline.set([...this.timeline(), ...state.current.timeline]);
    }
  }

  render(com, state) {
    // Different model based on tick count
    const model = state.tick > 5
      ? openai("gpt-5.2")      // Switch to powerful model
      : openai("gpt-5.2-mini"); // Start with fast model

    return (
      <>
        <Model model={model} />
        <System>You are a helpful assistant.</System>
        <Timeline>
          {this.timeline().map(entry => (
            <Message key={entry.id} {...entry.message} />
          ))}
        </Timeline>
      </>
    );
  }
}

Common Patterns

Pattern 1: Complexity-Based Upgrade

Start with a cheaper model, upgrade when the conversation gets complex:

tsx
class SmartRouterAgent extends Component {
  private complexity = signal<"low" | "medium" | "high">("low");

  async onTickEnd(com, state) {
    // Analyze the last assistant response
    const lastEntry = state.current?.timeline.at(-1);
    if (lastEntry?.message.role === "assistant") {
      const content = getTextContent(lastEntry.message);

      // Simple heuristics for complexity
      if (content.includes("```") || content.length > 2000) {
        this.complexity.set("high");
      } else if (content.length > 500) {
        this.complexity.set("medium");
      }
    }
  }

  render() {
    const complexity = this.complexity();

    const model = {
      low: openai("gpt-5.2-mini"),
      medium: openai("gpt-5.2"),
      high: anthropic("claude-3-5-sonnet"),
    }[complexity];

    return (
      <>
        <Model model={model} />
        <System>You are an expert assistant.</System>
        {/* ... */}
      </>
    );
  }
}

Pattern 2: Task-Based Selection

Choose models based on what the user is asking for:

tsx
class TaskRouterAgent extends Component {
  private taskType = signal<"chat" | "code" | "analysis">("chat");

  async onMessage(message) {
    const content = message.content.toLowerCase();

    if (content.includes("code") || content.includes("function") || content.includes("bug")) {
      this.taskType.set("code");
    } else if (content.includes("analyze") || content.includes("compare") || content.includes("evaluate")) {
      this.taskType.set("analysis");
    } else {
      this.taskType.set("chat");
    }
  }

  render() {
    const task = this.taskType();

    // Best model for each task
    const model = {
      chat: openai("gpt-5.2-mini"),       // Fast for simple chat
      code: anthropic("claude-3-5-sonnet"), // Great for code
      analysis: openai("gpt-5.2"),         // Good for analysis
    }[task];

    return (
      <>
        <Model model={model} />
        <System>
          {task === "code" && "You are an expert programmer."}
          {task === "analysis" && "You are a careful analyst."}
          {task === "chat" && "You are a helpful assistant."}
        </System>
        {/* ... */}
      </>
    );
  }
}

Pattern 3: Cost-Conscious Routing

Track token usage and switch to cheaper models when budget runs low:

tsx
class BudgetAgent extends Component {
  private totalTokens = signal(0);
  private budget = 100000; // Token budget

  async onTickEnd(com, state) {
    const usage = state.current?.usage;
    if (usage) {
      this.totalTokens.set(this.totalTokens() + usage.totalTokens);
    }
  }

  render() {
    const used = this.totalTokens();
    const remaining = this.budget - used;
    const percentUsed = (used / this.budget) * 100;

    // Downgrade as budget depletes
    let model;
    if (percentUsed < 50) {
      model = openai("gpt-5.2");
    } else if (percentUsed < 80) {
      model = openai("gpt-5.2-mini");
    } else {
      model = openai("gpt-5.2-mini");
    }

    return (
      <>
        <Model model={model} />
        <System>
          You are a helpful assistant.
          {percentUsed > 70 && " Please be concise to conserve resources."}
        </System>
        {/* ... */}
      </>
    );
  }
}

Pattern 4: Fallback on Error

Switch to a backup model if the primary fails:

tsx
class ResilientAgent extends Component {
  private primaryFailed = signal(false);
  private failureCount = signal(0);

  async onError(error, com) {
    if (error.code === "RATE_LIMIT" || error.code === "SERVICE_UNAVAILABLE") {
      this.failureCount.set(this.failureCount() + 1);

      if (this.failureCount() >= 2) {
        this.primaryFailed.set(true);
      }

      // Retry with fallback
      return { retry: true };
    }

    // Don't handle other errors
    throw error;
  }

  render() {
    const useFallback = this.primaryFailed();

    const model = useFallback
      ? anthropic("claude-3-haiku")  // Fallback
      : openai("gpt-5.2");             // Primary

    return (
      <>
        <Model model={model} />
        {useFallback && (
          <System note="internal">Using fallback model due to primary unavailability.</System>
        )}
        <System>You are a helpful assistant.</System>
        {/* ... */}
      </>
    );
  }
}

Pattern 5: User Tier Routing

Different models for different user tiers:

tsx
class TieredAgent extends Component {
  render(com) {
    const ctx = context();
    const tier = ctx.user?.tier || "free";

    const model = {
      free: openai("gpt-5.2-mini"),
      pro: openai("gpt-5.2-mini"),
      enterprise: openai("gpt-5.2"),
    }[tier];

    const maxTokens = {
      free: 1000,
      pro: 4000,
      enterprise: 16000,
    }[tier];

    return (
      <>
        <Model model={model} maxTokens={maxTokens} />
        <System>You are a helpful assistant.</System>
        {tier === "free" && (
          <System>Keep responses concise (under 500 words).</System>
        )}
        {/* ... */}
      </>
    );
  }
}

Model Configuration

The <Model> component accepts various configuration options:

tsx
<Model
  model={openai("gpt-5.2")}
  temperature={0.7}
  maxTokens={4096}
  topP={0.9}
  frequencyPenalty={0.5}
  presencePenalty={0.5}
  stopSequences={["END", "STOP"]}
/>

You can make any of these dynamic:

tsx
render(com, state) {
  // More creative as conversation progresses
  const temperature = Math.min(0.3 + (state.tick * 0.1), 1.0);

  return (
    <>
      <Model model={openai("gpt-5.2")} temperature={temperature} />
      {/* ... */}
    </>
  );
}

Multi-Model Architectures

Orchestrator Pattern

One model decides, another executes:

tsx
class OrchestratorAgent extends Component {
  private plan = comState<string[]>("plan", []);
  private currentStep = signal(0);

  render(com, state) {
    const plan = this.plan();
    const step = this.currentStep();

    if (plan.length === 0) {
      // Planning phase: use a reasoning model
      return (
        <>
          <Model model={openai("o3")} />
          <System>
            Create a step-by-step plan for the user's request.
            Output as JSON: ["step1", "step2", ...]
          </System>
          <PlanningTool onPlan={(steps) => this.plan.set(steps)} />
          {/* ... */}
        </>
      );
    }

    // Execution phase: use a fast model
    return (
      <>
        <Model model={openai("gpt-5.2-mini")} />
        <System>
          Execute step {step + 1}: {plan[step]}
        </System>
        <ExecutionTools onComplete={() => this.currentStep.set(step + 1)} />
        {/* ... */}
      </>
    );
  }
}

Verification Pattern

One model generates, another verifies:

tsx
class VerifiedAgent extends Component {
  private draft = signal<string | null>(null);
  private verified = signal(false);

  render() {
    const draft = this.draft();
    const verified = this.verified();

    if (!draft) {
      // Generation phase
      return (
        <>
          <Model model={openai("gpt-5.2")} />
          <System>Generate a response to the user's question.</System>
          <DraftTool onDraft={(d) => this.draft.set(d)} />
          {/* ... */}
        </>
      );
    }

    if (!verified) {
      // Verification phase
      return (
        <>
          <Model model={anthropic("claude-3-5-sonnet")} />
          <System>
            Verify this response for accuracy. If correct, approve it.
            If incorrect, explain the issues.
          </System>
          <Grounding title="Draft to Verify">{draft}</Grounding>
          <VerifyTool
            onApprove={() => this.verified.set(true)}
            onReject={() => this.draft.set(null)}
          />
        </>
      );
    }

    // Verified - return the draft
    return <AssistantMessage>{draft}</AssistantMessage>;
  }
}

Key Takeaways

  1. Model selection is dynamic: Happens on every tick in render()
  2. Use signals to track context: Complexity, task type, budget, errors
  3. Multiple models can work together: Orchestration, verification, fallback
  4. Configuration is also dynamic: Temperature, max tokens, etc.
  5. Match model to task: Not every request needs the most powerful model

Next Steps

Released under the MIT License.