{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "openrouter-intro",
      "metadata": {},
      "source": "# Switch models through OpenRouter\n\nKeep one OpenRouter credential and switch model slugs from different providers through `aner.llm`. This notebook uses the same prompt, explicit history, output budget and temperature for two requests. The examples are [OpenAI GPT 4.1 Mini](https://openrouter.ai/openai/gpt-4.1-mini) and [Google Gemma 3 27B](https://openrouter.ai/google/gemma-3-27b-it); replace them with model slugs available to your account. OpenRouter controls model availability, pricing and upstream routing.\n\nUse an updated Aner runtime with `secrets.dotenv` and the complete token usage accessors and the Aner notebook kernel. Review the credential source and model choices before **Run All**: running all cells sends **two requests**, which may incur charges. Repeating either request cell sends another request. No credentials or saved execution output are included. Read [Language model connections](https://anerlang.org/docs/llm) and [Credentials](https://anerlang.org/docs/credentials).\n"
    },
    {
      "cell_type": "markdown",
      "id": "openrouter-credentials",
      "metadata": {},
      "source": "## 1. Choose a private credential source\n\nThe default is `key_source = \"stored\"`. Set the credential through hidden terminal input before running request cells:\n\n```sh\naner secrets set openrouter\n```\n\nAlternatively, set `key_source = \"dotenv\"` and select **one** `env_file`: `.env`, `.env.production`, `.env.development` or `.env.local`. `secrets.dotenv(path: env_file, name: \"OPENROUTER_API_KEY\")` returns an opaque `SecretRef`; `aner.llm` resolves the selected key only when sending a request. No file search, precedence, merging, variable expansion or process environment mutation occurs. Stored and dotenv sources are alternatives; a missing selected credential fails instead of falling back.\n\nCreate the selected file privately outside this notebook. Its line should have this form; replace the placeholder privately:\n\n```dotenv\nOPENROUTER_API_KEY=replace-with-your-key\n```\n\nKeep the file outside version control. On macOS/Linux, use a regular file owned by you with mode `600` (`chmod 600 .env.local`, adjusted for the chosen filename); symlinks and hard linked credential files are rejected. Relative paths are captured against the kernel's working directory when the reference is created, which can differ from the notebook folder. Use an absolute path if needed. An existing injected environment variable can instead be referenced explicitly with `secrets.env(\"OPENROUTER_API_KEY\")`.\n\nA reference never places the key value in notebook source or output. Each request resolves it again, so a privately rotated key is picked up without embedding it in the notebook. After changing the source or filename, restart the session and run setup cells in order. On headless Linux, see the Credentials guide for managed file storage (`secrets.file(\"openrouter\")`).\n"
    },
    {
      "cell_type": "code",
      "id": "openrouter-credential-setup",
      "metadata": {},
      "source": [
        "import aner.llm\n",
        "import aner.secrets\n",
        "\n",
        "// Use exactly \"stored\" or \"dotenv\". Never put the private key here.\n",
        "let key_source = \"stored\"\n",
        "let env_file = \".env.local\" // Or .env, .env.production, .env.development; absolute paths also work.\n",
        "var openrouter_key = secrets.stored(\"openrouter\")\n",
        "if key_source == \"dotenv\" {\n",
        "    openrouter_key = secrets.dotenv(path: env_file, name: \"OPENROUTER_API_KEY\")\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "openrouter-experiment",
      "metadata": {},
      "source": "## 2. Set the model and shared experiment\n\nEdit `selected_model` to a model slug from [OpenRouter's model catalog](https://openrouter.ai/models). Both request cells use the exact `messages` list below; no previous answer is added automatically. Client construction and this setup send no requests and do not read the credential.\n\nThe 256 token output budget bounds each answer, and `finish_reason()` reports whether the service stopped normally or reached the limit. Temperature support and token accounting depend on the selected model. Identical settings do not guarantee identical answers or equivalent token counts across models.\n"
    },
    {
      "cell_type": "code",
      "id": "openrouter-experiment-setup",
      "metadata": {},
      "source": [
        "var selected_model = \"openai/gpt-4.1-mini\"\n",
        "let output_budget = 256\n",
        "let sampling_temperature = 0.2\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",
        "print(\"Selected OpenRouter model:\")\n",
        "print(selected_model)\n",
        "\n",
        "// Missing usage is unavailable, never zero. Zero remains a valid reported count.\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": "openrouter-first-request",
      "metadata": {},
      "source": "## 3. Ask the selected model\n\nRun the next cell after configuring the selected credential. It sends one explicit request to OpenRouter and prints the answer, requested and returned model identifiers, finish reason and reported token usage. The helper displays missing usage (`-1`) as `unavailable` and preserves a reported zero. Input/output/total come from the provider tokenizer. `Cache-read/cache-write` counts are already included in input, and reasoning is already included in output: **do not add these details again**. Counts cover this request and its supplied history; they do not verify the amount charged. Persist the usage with provider/model identifiers for experiment accounting, and consult OpenRouter’s billing records for money charged. The block keeps response variables local so this request cell can be rerun in a persistent session.\n"
    },
    {
      "cell_type": "code",
      "id": "openrouter-first-call",
      "metadata": {},
      "source": [
        "// One request. Rerunning this cell sends another request.\n",
        "{\n",
        "    let model = llm.client(provider: \"openrouter\", model: selected_model,\n",
        "        key: openrouter_key)\n",
        "    let answer = model.chat(messages, max_tokens: output_budget,\n",
        "        temperature: sampling_temperature)\n",
        "    print(\"Requested model:\")\n",
        "    print(model.model())\n",
        "    print(\"Answer:\")\n",
        "    print(answer.text())\n",
        "    print(\"Returned model:\")\n",
        "    print(answer.model())\n",
        "    print(\"Finish reason:\")\n",
        "    print(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",
        "    report_tokens(\"Cached input tokens (included in input)\", answer.cached_input_tokens())\n",
        "    report_tokens(\"Cache-write input tokens (included in input)\", answer.cache_write_input_tokens())\n",
        "    report_tokens(\"Reasoning tokens (included in output)\", answer.reasoning_tokens())\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "openrouter-switch",
      "metadata": {},
      "source": "## 4. Switch to a model from another provider\n\nKeep `provider: \"openrouter\"` and the same credential reference. Change only the organization/model slug. This assignment makes no network call; the following request cell uses the new selection with the same history and generation controls.\n"
    },
    {
      "cell_type": "code",
      "id": "openrouter-switch-model",
      "metadata": {},
      "source": [
        "selected_model = \"google/gemma-3-27b-it\"\n",
        "print(\"Selected OpenRouter model:\")\n",
        "print(selected_model)\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "id": "openrouter-second-call",
      "metadata": {},
      "source": [
        "// One request. Rerunning this cell sends another request.\n",
        "{\n",
        "    let model = llm.client(provider: \"openrouter\", model: selected_model,\n",
        "        key: openrouter_key)\n",
        "    let answer = model.chat(messages, max_tokens: output_budget,\n",
        "        temperature: sampling_temperature)\n",
        "    print(\"Requested model:\")\n",
        "    print(model.model())\n",
        "    print(\"Answer:\")\n",
        "    print(answer.text())\n",
        "    print(\"Returned model:\")\n",
        "    print(answer.model())\n",
        "    print(\"Finish reason:\")\n",
        "    print(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",
        "    report_tokens(\"Cached input tokens (included in input)\", answer.cached_input_tokens())\n",
        "    report_tokens(\"Cache-write input tokens (included in input)\", answer.cache_write_input_tokens())\n",
        "    report_tokens(\"Reasoning tokens (included in output)\", answer.reasoning_tokens())\n",
        "}\n"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "id": "openrouter-next-steps",
      "metadata": {},
      "source": "## Try another model deliberately\n\nAssign a different available slug to `selected_model`, then rerun either request cell. To return to the first model, run `selected_model = \"openai/gpt-4.1-mini\"` in a new cell. Each request resolves the same private credential reference at execution time. Changing a configured slug leaves already displayed results as historical output; inspect the printed requested and returned model identifiers when comparing answers.\n\nFor a fresh full run after editing setup declarations, restart the notebook session and run cells in order. Credentials remain in their selected store or private dotenv file. Production mode disables development recording; it does not disable inference or provider billing. Prompt Strings and explicitly printed answer text remain ordinary notebook data: review and clear output before sharing. This notebook contains no optional batch loop, so **Run All** sends exactly the two demonstrated requests.\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
}
