# Fase 1a — første ende-til-ende-kjøring mot en levende modell (lokal) **Dato:** 2026-08-13 · **Tre:** `5871e39` (v1.0.0 + de to presentasjonsfilene) · **Profil:** `local` Rammeverket hadde aldri kjørt mot en levende språkmodell — bare mot skriptede stand-ins. Denne rapporten er den første målingen av hva som faktisk skjer når en ekte modell settes inn i loopen. Fase 1a er den **gratis** halvdelen: en lokal Ollama-kjøring feller alt som ikke er autentisering, slik at den betalte Foundry-kjøringen (1b) bare har RBAC, token og deployment-navn igjen å feile på. > **Ærlighetsgrense.** Alt under er målt på ÉN maskin (Intel i9-9880H, CPU-only) med SMÅ > lokale modeller. Tallene beskriver denne maskinen og disse modellene — ikke rammeverkets > ytelse generelt, og ikke hva en Foundry-modell vil gjøre. ## Miljø | | | |---|---| | Maskin | Intel Core i9-9880H, 16 logiske kjerner, ingen GPU-akselerasjon for Ollama | | Ollama | 0.16.3 | | Python / MAF | 3.12.13 / `agent-framework-core` 1.9.0 | | Kunnskapsbase | `shared/examples/bygg-energi-mikro`, prosjekt `BYGG-KONTOR-NORD` | | Tak | `max_rounds=3`, `max_tokens=100000` (kompilert inn, se funn 3) | ## Stigen: fire trinn, der de tre første er gratis Rekkefølgen er ikke tilfeldig. Hvert trinn gjør neste trinns feil attribuerbar. | # | Hva | Utfall | |---|---|---| | 1 | Endepunkt nås (`GET /api/tags`) | **http 200** | | 2 | `--live-dry-run` — bygger klient, stopper før første kall | **exit 0**, `models={proposer: qwen3:4b, checker: qwen3:4b}` | | 3 | `--scripted-replies` — HELE loopen, null modellkall | **exit 0**, `ValidatedProposal (decision=approved)` | | 4 | Ekte kjøring mot levende modell | se under | Trinn 3 er det som gjør rapporten verdt noe: det beviser at argumentrekka, kunnskapsbasen, debatten, den deterministiske validatoren og domsavsigelsen henger sammen **uten** en modell i bildet. Enhver feil i trinn 4 er dermed forårsaket av levende modell-output, ikke av oppsettet. Merk at et grønt trinn 2 **ikke** beviser at endepunktet virker: `--live-dry-run` bygger klienten og stopper før første kall, så det går grønt også med serveren nede. Trinn 1 er derfor en egen måling, ikke en formalitet. ## Kjøring 1 — den shippede konfigurasjonen (`qwen3:4b`) ``` uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD --profile local \ --docs-dir shared/examples/bygg-energi-mikro \ --bundle-dir shared/examples/bygg-energi-mikro ``` **Utfall: exit 1 etter 1804 sekunder (30 minutter), null forslag produsert.** ``` agent_framework.exceptions.ChatClientException: OpenAIChatCompletionClient service failed to complete the prompt: Request timed out. (APITimeoutError) ``` Årsakskjeden er målt hele veien, ikke resonnert: 1. **Ollamas logg viser tre kall, hvert avbrutt på nøyaktig `10m0s`.** 3 × 600 s = 1800 s, som er de 1804 sekundene kjøringen brukte. Det er OpenAI-SDK-ens standard timeout på 600 s pluss dens to standard retries — ingen av delene er valgt av oss (funn 1). 2. **Modellen rakk aldri å svare.** Målt direkte på samme maskin: `qwen3:4b` genererer **4,0 tokens/sekund**. Et 600-sekunders vindu rommer altså ~2400 tokens. 3. **Og den brukte alt på å tenke.** `qwen3` er en resonnerende modell. I en kontrollmåling med 256 tokens til rådighet var svaret **tomt** — hele budsjettet gikk med til den interne tankerekka ``, før ett eneste tegn av det faktiske svaret ble skrevet (funn 2). ## Funn ### Funn 1 — den lokale klienten har ingen timeout-søm (rammeverket) `backends.py:130` konstruerer `OpenAIChatCompletionClient(model=..., api_key=..., base_url=...)`. Målt mot klassens signatur: den tar **ingen** `timeout`- eller `max_retries`-parameter. Eneste vei inn er å bygge og sende inn en egen `async_client`. Konsekvensen er at enhver maskin som er tregere enn modellen krever, feiler etter 30 minutter med en melding som ser ut som et nettverksproblem. Det er ikke en nettverksfeil — det er et tak ingen har valgt. **Ikke fikset her.** Det er produksjonskode og krever en feilende test først (Iron Law), altså egen økt. Formen på fiksen er kjent: la `create_chat_client` bygge `AsyncOpenAI(timeout=…, max_retries=…)` med verdier fra env, og la en load-bearing test bli rød når sømmen kobles fra. ### Funn 2 — en resonnerende modell bruker hele budsjettet på å tenke Dette er ikke en defekt i rammeverket, men det er en **forutsetning ingen hadde skrevet ned**: modell-mappets lokale rolle pekte på `qwen3:4b`, og en resonnerende modell på treg maskinvare når aldri fram til svaret. For den lokale profilen bør standardvalget være en modell **uten** tankemodus — eller tankemodus må skrus av eksplisitt. ### Funn 3 — planens token-tak-mekanisme finnes ikke i CLI-en Planens fase 1b foreskriver «harde token-tak (`--max-*`-flaggene i `run.py`)». **Målt: slike flagg finnes ikke.** Takene er kompilert inn — `_DEFAULT_MAX_ROUNDS = 3`, `_DEFAULT_MAX_TOKENS = 100_000` (`run.py:110-111`) — og kan ikke settes fra kommandolinjen. Gratis og uvesentlig lokalt. **For 1b er det bærende:** en betalt kjøring vil ellers gå under et 100 000-tokens tak ingen har tatt stilling til. Dette er en beslutning for operatøren før den første Foundry-kjøringen, ikke noe som skal oppdages under den. ### Funn 4 — kunnskapsbasen sprengte standard kontekstvindu, stille Ollamas logg, kjøring 1: ``` level=WARN msg="truncating input prompt" limit=4096 prompt=4388 keep=4 new=4096 ``` Ollamas standard kontekstvindu er 4096 tokens; prompten fra den navigerte kunnskapsbasen er 4388. Overskytende ble **kuttet** — og `keep=4` betyr at bare fire tokens fra starten ble bevart, altså at instruksjonene om svarformat sto først i det som røk. Advarselen står i Ollamas logg, ikke i vår. En operatør som kjører lokalt ser den ikke. Rettet for kjøring 2 ved å starte serveren med `OLLAMA_CONTEXT_LENGTH=16384`; at dette må gjøres bør stå i den lokale oppskriften. ## Kjøring 2 — med funn 2 og 4 kompensert Modell byttet til `qwen2.5:3b` (**uten** tankemodus) via en modell-map utenfor treet (`PORTFOLIO_MODEL_MAP`), og serveren restartet med `OLLAMA_CONTEXT_LENGTH=16384`. Repoets egen konfig er urørt — dette er en erklært overstyring, ikke den shippede oppsettet. **Utfall: samme vegg.** To kall, begge avbrutt på nøyaktig `10m0s`. Kjøringen ble stoppet manuelt framfor å brenne det tredje forsøket mot et kjent utfall. Ett funn ble likevel lukket: **null trunkering** i loggen. Det større kontekstvinduet løser funn 4. **Diagnosen flyttet seg, og det er den viktigste setningen i rapporten:** flaskehalsen er ikke tankemodusen. Det er at 600-sekunders-taket er bindende på denne maskinvaren **uansett modell**. En kontrollmåling med `llama3.2:1b` — en modell på 1,3 GB — brukte også over ti minutter på ett kall med denne kontekststørrelsen, mens maskinen strupet seg fra 62 % til 54 % klokkefrekvens. ## Hva som er bevist, og hva som ikke er **Bevist:** loopen henger sammen fra ende til annen og produserer et validert forslag — men med skriptede svar, null modellkall (trinn 3). **IKKE bevist:** at en levende modell produserer noe loopen kan konsumere. **Null vellykkede modellkall.** Dermed står disse fortsatt åpne, og de var hele grunnen til at 1a finnes: - om prompt-formene gir parsebar JSON fra en ekte modell - om checkeren faktisk avslutter med `VERDICT:`-linja - om runde-taket oppfører seg som forutsatt - om validatoren avviser kandidater av riktig grunn ## Hva dette betyr for 1b **Fase 1a har levert funn, ikke beviset.** Den lokale halvdelen kan ikke fullføre på denne maskinvaren uten at funn 1 fikses først — timeout-sømmen er dermed en *forutsetning* for lokal ende-til-ende, ikke en forbedring. To veier videre, og de utelukker ikke hverandre: 1. **Bygg timeout-sømmen** (egen økt, feilende test → fiks → målt mutasjon). Da blir lokal profil kjørbar på treg maskinvare, og 1b arver en klient med et tak noen har valgt. 2. **Gå til 1b.** En Foundry-modell svarer på sekunder, ikke minutter — 600 s er ikke bindende der. Ende-til-ende blir da bevist mot ekte modell, som er det operatøren opprinnelig ba om. **Kostnad er ikke en begrensning for 1b.** Regnet på Anthropics egen prisliste for Claude Haiku 4.5 ($1,00/MTok inn, $5,00/MTok ut — Claude er tilgjengelig på Microsoft Foundry til standard API-priser): ~54 000 input-tokens + ~5 000 output-tokens per full kjøring ≈ **$0,08**, altså under én krone. Ti kjøringer er en tier. Det som gjenstår for 1b er ikke penger, men Azure-oppsettet: ressurs, prosjekt, én billig deployment og rollen `Foundry User` — portal-steg bare operatøren kan gjøre. **Uverifisert, må måles før det bygges på:** om MAFs `FoundryChatClient` kan binde en Claude-deployment. Foundry-tilgjengeligheten er dokumentert; klient-kompatibiliteten er det ikke. ## Verifiseringslogg | Påstand | Kilde | |---|---| | Trinn 1–3 grønne; trinn 4 exit 1 etter 1804 s | egne kjøringer, stdout/stderr fanget ordrett | | 3 × 600 s timeout + 2 retries er SDK-standard | Ollamas logg (`500 \| 10m0s` × 3) + `agent_framework` traceback | | Klienten har ingen `timeout`-parameter | `inspect.signature(OpenAIChatCompletionClient.__init__)` | | `qwen3:4b` gir 4,0 tok/s; 256 tokens ga tomt svar | `/api/generate` med `eval_count`/`eval_duration` | | Prompt 4388 tokens, kuttet mot 4096 | Ollamas `truncating input prompt`-advarsel | | `_DEFAULT_MAX_ROUNDS = 3`, `_DEFAULT_MAX_TOKENS = 100_000` | `run.py:110-111`; CLI-en har ingen `--max-*` | | Haiku 4.5-priser; Claude på Foundry til standard API-rater | Anthropics modell-/prisdokumentasjon (`claude-api`-skillen) |