{"runtime":"ollama","title":"Ollama and other local models — gate the tool, not the model","language":"python","filename":"ratchet_ollama.py","install":"pip install ollama requests","code":"import os, ollama, requests\n\nBASE = \"https://ratchetgate.com\"\nH = {\"Authorization\": f\"Bearer {os.environ['RATCHET_API_KEY']}\"}\n\nTOOLS = [{\"type\": \"function\", \"function\": {\n    \"name\": \"issue_refund\",\n    \"description\": \"Refund a customer order. Moves real money.\",\n    \"parameters\": {\"type\": \"object\", \"properties\": {\n        \"order_id\": {\"type\": \"string\"}, \"amount_usd\": {\"type\": \"number\"}},\n        \"required\": [\"order_id\", \"amount_usd\"]}}}]\n\n\ndef handle(call):\n    a = call[\"function\"][\"arguments\"]\n    # The gate sits HERE — in your code, around the tool. Not in the model,\n    # which is why it works identically for a 7B local model and a frontier one.\n    d = requests.post(f\"{BASE}/v1/effects/begin\", headers=H, json={\n        \"effect_type\": \"payment.refund\",\n        \"idempotency_key\": f\"refund:{a['order_id']}:{a['amount_usd']}\",\n        \"payload\": a,\n        \"estimated_cost_micros\": int(a[\"amount_usd\"] * 1_000_000),\n    }, timeout=10).json()\n\n    if d[\"decision\"] != \"execute\":\n        return f\"BLOCKED: {d['decision']}. Already initiated. Do not retry.\"\n\n    do_the_refund(a)\n    requests.post(f\"{BASE}/v1/effects/{d['effect_id']}/report\", headers=H,\n                  json={\"lease_token\": d[\"lease_token\"], \"outcome\": \"succeeded\"}, timeout=10)\n    return \"refunded\"\n\n\nmsgs = [{\"role\": \"user\", \"content\": \"Refund order A-771 for $49.99.\"}]\nr = ollama.chat(model=\"hermes3:8b\", messages=msgs, tools=TOOLS)\nfor call in r[\"message\"].get(\"tool_calls\", []):\n    msgs.append({\"role\": \"tool\", \"content\": handle(call)})","notes":["The duplicate usually is not the model changing its mind — it is the run being restarted: a job retried, a queue redelivered, a process that acted and crashed before recording it. The gate is what survives that, because it is not in the process.","Derive the idempotency key from the work itself — the order, the recipient, the period. Never from a UUID, a timestamp, or a retry counter: those differ on every attempt, so the gate would authorise every one of them."],"next":{"create_key":"https://ratchetgate.com/console","docs":"https://ratchetgate.com/docs"}}