portfolio-optimiser/docs/2026-08-13-fase1a-lokal-ende-til-ende.md
Kjell Tore Guttormsen cb809b683c docs(fase1a): «uansett modell» hvilte på en måling jeg aldri leste ferdig
Advisor felte påstanden ETTER at forrige commit var pushet. Rapporten sa at
600 s-taket er bindende «UANSETT modell», med `llama3.2:1b brukte OGSÅ >10 min»
som belegg. Men den målingen sprengte Bash-timeouten, gikk til bakgrunnen, og
ble aldri lest til slutt — «>10 min» var det jeg SÅ ved én kikk, ikke et resultat.

Målt nå ved manuell stopp: prosessen sto på 18 min 54 s UTEN å fullføre, og
loggen har ingen `POST "/api/generate"` for kallet. Det gir en NEDRE grense
(kallet oversteg taket med god margin) og ingen øvre.

Påstanden er derfor snevret til «på denne maskinen i denne tilstanden», med
konfunderingen uttalt: CPU-en strupet seg 62 % → 54 % underveis, og to fremmede
Python-prosesser holdt ~1,8 kjerner. Et generelt utsagn om modellstørrelse ville
krevd en ren maskin og en fullført måling; ingen av delene finnes her.
Verifiseringsloggen har fått raden + en eksplisitt «ikke verifisert»-note.

Samme defektklasse som grep-en samme økt, ett nivå opp: der var instrumentet i
stykker, her var det i orden og jeg leste det bare aldri.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X737cbkG2uAXJ2Bvhf6X5M
2026-08-13 20:19:31 +02:00

196 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `<think>`, 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 maskinen i denne tilstanden**,
også for modeller som er langt mindre enn den shippede.
Kontrollmålingen: `llama3.2:1b` — en modell på 1,3 GB — sto **18 min 54 s uten å fullføre ett
enkelt kall** med denne kontekststørrelsen, og ble så stoppet manuelt. **Presist hva dette er:**
den nedre grensen er målt (kallet overskred 600-sekunders-taket med god margin), men den øvre er
det ikke — målingen ble aldri lest til slutt, så det finnes ikke noe totaltall for den.
**Og påstanden er bevisst snevret til denne maskinen i denne tilstanden**, ikke «uansett modell»:
målingen er konfundert av at CPU-en strupet seg fra 62 % til 54 % klokkefrekvens underveis, og at
to fremmede Python-prosesser holdt ~1,8 kjerner samtidig. Et generelt utsagn om modellstørrelse
ville krevd en ren maskin og en fullført måling; ingen av delene finnes her.
## 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 13 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) |
| `llama3.2:1b` 18 min 54 s **uten å fullføre** — nedre grense, ikke totaltid | `ps -o etime=` ved manuell stopp; ingen `POST "/api/generate"` i loggen for kallet |
**Ikke verifisert, uttalt:** at taket ville vært bindende på en URØRT maskin, eller for en vilkårlig
liten modell. Kontrollmålingen ble kjørt under termisk struping og fremmed last, og ble aldri
fullført. Påstandene over gjelder denne maskinen i denne tilstanden.