{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "provider-switching-intro",
      "metadata": {},
      "source": "# Switch between Anthropic, Gemini and OpenRouter\n\nUse the same prompt and explicit history with three native `aner.llm` adapters, keeping one private credential reference for each provider in `aner.secrets`. Every request prints its provider, requested and returned model identifiers, finish reason, and **input, output and total tokens**.\n\nUse a compatible updated native development runtime and matching Aner editor/notebook kernel; see [installation and availability](https://anerlang.org/docs/environment). This notebook uses persistent session execution and production mode. Review credentials and model choices before **Run All**: all cells send **three requests**, which may incur charges. Rerunning one request cell sends another request. No credentials or saved output are included. Read [Language model connections](https://anerlang.org/docs/llm) and [Credentials](https://anerlang.org/docs/credentials).\n"
    },
    {
      "cell_type": "markdown",
      "id": "provider-switching-credentials",
      "metadata": {},
      "source": "## 1. Keep three private keys in `aner.secrets`\n\nThe default is `key_source = \"stored\"`. Configure three distinct entries through hidden terminal input before running request cells:\n\n```sh\naner secrets set anthropic\naner secrets set gemini\naner secrets set openrouter\n```\n\nRun these commands in a terminal; never paste a key into a notebook cell or command argument. `secrets.stored` uses the supported operating system credential service. For headless Linux, choose injected environment variables below or follow the Credentials guide for the explicit managed file store.\n\nAlternatively set `key_source = \"env\"` to reference existing injected `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` and `OPENROUTER_API_KEY` variables. To use a private project file, set `key_source = \"dotenv\"` and select exactly one `env_file`: `.env`, `.env.production`, `.env.development` or `.env.local` (an absolute path also works). Create its three assignments privately outside this notebook:\n\n```dotenv\nANTHROPIC_API_KEY=replace-with-your-anthropic-key\nGEMINI_API_KEY=replace-with-your-gemini-key\nOPENROUTER_API_KEY=replace-with-your-openrouter-key\n```\n\nThe placeholders show the format only. Keep actual values in the selected private store or file, outside source, notebook output and version control. On macOS/Linux, use a regular file owned by you with mode `600` (`chmod 600 .env.local`, adjusted for your filename); symbolic links and hard linked credential files are rejected. See the [complete file contract](https://anerlang.org/docs/credentials#explicit-project-environment-files).\n\n`SecretRef` values are opaque; constructing them reads no key and sends no request. `aner.llm` resolves only the selected reference when sending its request. There is no file discovery, merging, variable expansion, environment mutation or fallback to another source. Relative paths are captured against the kernel's working directory at construction, which may differ from the notebook folder. Restart the session and run setup in order after changing the source or path.\n"
    },
    {
      "cell_type": "code",
      "id": "provider-switching-credential-setup",
      "metadata": {},
      "source": [
        "import aner.llm\n",
        "import aner.secrets\n",
        "\n",
        "// Select exactly \"stored\", \"env\" or \"dotenv\". Never place a private key here.\n",
        "let key_source = \"stored\"\n",
        "let env_file = \".env.local\" // Or .env, .env.production, .env.development; absolute paths work.\n",
        "var anthropic_key = secrets.stored(\"anthropic\")\n",
        "var gemini_key = secrets.stored(\"gemini\")\n",
        "var openrouter_key = secrets.stored(\"openrouter\")\n",
        "if key_source == \"dotenv\" {\n",
        "    anthropic_key = secrets.dotenv(path: env_file, name: \"ANTHROPIC_API_KEY\")\n",
        "    gemini_key = secrets.dotenv(path: env_file, name: \"GEMINI_API_KEY\")\n",
        "    openrouter_key = secrets.dotenv(path: env_file, name: \"OPENROUTER_API_KEY\")\n",
        "}\n",
        "if key_source == \"env\" {\n",
        "    anthropic_key = secrets.env(\"ANTHROPIC_API_KEY\")\n",
        "    gemini_key = secrets.env(\"GEMINI_API_KEY\")\n",
        "    openrouter_key = secrets.env(\"OPENROUTER_API_KEY\")\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "provider-switching-experiment",
      "metadata": {},
      "source": "## 2. Share the prompt, history and output budget\n\nEach request uses exactly the same `messages` list and a 1024 token output budget. No earlier answer is added automatically. Temperature is omitted because the Gemini stable v1 adapter does not accept it and some Anthropic models restrict sampling controls. Each service applies its defaults; identical prompts and budgets do not make answers or token counts equivalent across models.\n\nThe helper treats missing usage (`-1`) as `unavailable` and preserves a reported zero. The total comes from the response's `total_tokens()` accessor, which retains a provider total when reported and otherwise derives a checked sum only when input and output are known. Do not estimate tokens from answer length or treat an unknown count as zero. Input already includes reported cache read/write tokens; output already includes reported reasoning tokens. Do not add those details again. Usage covers one call's submitted history and generation, rather than a conversation wide running total. Provider billing records remain authoritative for charges.\n"
    },
    {
      "cell_type": "code",
      "id": "provider-switching-experiment-setup",
      "metadata": {},
      "source": [
        "let output_budget = 1024\n",
        "let messages = List(\n",
        "    llm.message(role: \"user\", content: \"Explain precisely and concisely in three sentences why preprocessing must be fitted on training data only.\"))\n",
        "\n",
        "fn report_tokens(label: String, count: Int64) {\n",
        "    if count < 0 {\n",
        "        print(label + \": unavailable\")\n",
        "    } else {\n",
        "        print(label + \": \" + String(count))\n",
        "    }\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "provider-switching-anthropic",
      "metadata": {},
      "source": "## 3. Ask Anthropic\n\nThe next cell sends one request directly through the native Anthropic Messages adapter using `anthropic_key`. The model identifier is illustrative: replace it with a model available to your Anthropic account. Its local block keeps response variables separate so the cell can be rerun in a persistent session.\n"
    },
    {
      "cell_type": "code",
      "id": "provider-switching-anthropic-call",
      "metadata": {},
      "source": [
        "// One request. Rerunning this cell sends another request.\n",
        "{\n",
        "    let model = llm.client(provider: \"anthropic\", model: \"claude-sonnet-5-5\",\n",
        "        key: anthropic_key)\n",
        "    let answer = model.chat(messages, max_tokens: output_budget)\n",
        "    print(\"Provider: \" + model.provider())\n",
        "    print(\"Requested model: \" + model.model())\n",
        "    print(\"Answer:\")\n",
        "    print(answer.text())\n",
        "    print(\"Returned model: \" + answer.model())\n",
        "    print(\"Finish reason: \" + answer.finish_reason())\n",
        "    report_tokens(\"Input tokens\", answer.input_tokens())\n",
        "    report_tokens(\"Output tokens\", answer.output_tokens())\n",
        "    report_tokens(\"Total tokens\", answer.total_tokens())\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "provider-switching-gemini",
      "metadata": {},
      "source": "## 4. Ask Google Gemini\n\nSwitch the provider, model and credential reference together. This cell sends one request through the native Google Gemini stable v1 Interactions adapter using `gemini_key`, with the same history and output budget. The model identifier is illustrative; select one available to your Google account.\n"
    },
    {
      "cell_type": "code",
      "id": "provider-switching-gemini-call",
      "metadata": {},
      "source": [
        "// One request. Rerunning this cell sends another request.\n",
        "{\n",
        "    let model = llm.client(provider: \"gemini\", model: \"gemini-3.7-flash\",\n",
        "        key: gemini_key)\n",
        "    let answer = model.chat(messages, max_tokens: output_budget)\n",
        "    print(\"Provider: \" + model.provider())\n",
        "    print(\"Requested model: \" + model.model())\n",
        "    print(\"Answer:\")\n",
        "    print(answer.text())\n",
        "    print(\"Returned model: \" + answer.model())\n",
        "    print(\"Finish reason: \" + answer.finish_reason())\n",
        "    report_tokens(\"Input tokens\", answer.input_tokens())\n",
        "    report_tokens(\"Output tokens\", answer.output_tokens())\n",
        "    report_tokens(\"Total tokens\", answer.total_tokens())\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "provider-switching-openrouter",
      "metadata": {},
      "source": "## 5. Ask OpenRouter\n\nSwitch to the native OpenRouter adapter and its own `openrouter_key`. OpenRouter uses an organization/model slug; choose one available to your account. Switching model slugs within OpenRouter keeps `provider: \"openrouter\"` and the same OpenRouter credential reference. OpenRouter controls upstream routing and model availability.\n"
    },
    {
      "cell_type": "code",
      "id": "provider-switching-openrouter-call",
      "metadata": {},
      "source": [
        "// One request. Rerunning this cell sends another request.\n",
        "{\n",
        "    let model = llm.client(provider: \"openrouter\", model: \"openai/gpt-4.1-mini\",\n",
        "        key: openrouter_key)\n",
        "    let answer = model.chat(messages, max_tokens: output_budget)\n",
        "    print(\"Provider: \" + model.provider())\n",
        "    print(\"Requested model: \" + model.model())\n",
        "    print(\"Answer:\")\n",
        "    print(answer.text())\n",
        "    print(\"Returned model: \" + answer.model())\n",
        "    print(\"Finish reason: \" + answer.finish_reason())\n",
        "    report_tokens(\"Input tokens\", answer.input_tokens())\n",
        "    report_tokens(\"Output tokens\", answer.output_tokens())\n",
        "    report_tokens(\"Total tokens\", answer.total_tokens())\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "provider-switching-next-steps",
      "metadata": {},
      "source": "## Compare the printed call metadata\n\nUse the provider, requested/returned model identifiers, finish reason and three token counts to identify each result. A token budget finish can mean incomplete text. Keep unavailable counts unknown when accumulating usage, and retain each call's metadata alongside your own experiment identifier. Provider tokenizers and accounting rules differ, so token counts alone do not measure answer quality or monetary cost.\n\nTo try another model, edit the model identifier in its provider's request cell and rerun that cell. Each call resolves the corresponding private credential again, so privately rotated values apply to later requests. Changing a model or key reference leaves earlier displayed results as historical output; use the printed metadata to identify them. The request cells are independently rerunnable after setup. Restart the session before rerunning edited setup declarations.\n\nProduction mode disables development recording; provider requests and billing still occur. Prompt Strings and explicitly printed answer text remain ordinary notebook data. Review and clear output before sharing. This notebook has no batch loop or automatic retries: **Run All sends exactly three demonstrated requests**. Client construction and setup send none. The [plain Aner companion](https://anerlang.org/examples/llm-provider-switching) performs the same three requests when run.\n"
    }
  ],
  "metadata": {
    "aner": {
      "execution_mode": "persistent-session",
      "runtime_mode": "production"
    },
    "kernelspec": {
      "display_name": "Aner",
      "language": "aner",
      "name": "aner"
    },
    "language_info": {
      "name": "aner",
      "file_extension": ".aner"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
