docs(fase1a): lokal ende-til-ende feller på et 600 s-tak ingen har valgt

Fase 1a kjørt, og den leverte funn — ikke beviset. Stigen: endepunkt (http 200)
→ --live-dry-run (exit 0) → --scripted-replies (HELE loopen, null modellkall,
ValidatedProposal) → ekte modell. De tre første grønne; den fjerde exit 1.

Trinn 3 er det som gjør rapporten verdt noe: loopen er bevist sammenhengende UTEN
en modell i bildet, så feilen i trinn 4 er attribuerbar til levende modell-output.

Fire funn, alle målt:
1. Den lokale klienten har ingen timeout-søm (backends.py:130) — kjøringen døde
   etter 3 x 600 s = 30 min på SDK-standardverdier. Ikke fikset her: produksjons-
   kode krever feilende test først.
2. qwen3:4b er resonnerende og brukte hele budsjettet på tankerekka — målt 4,0
   tok/s, og 256 tokens ga TOMT svar.
3. Planens `--max-*`-flagg for token-tak finnes ikke; takene er kompilert inn.
   Inert lokalt, bærende for 1b: en betalt kjøring ville gått under et 100k-tak
   ingen har valgt.
4. Prompten (4388 tokens) ble STILLE kuttet mot Ollamas 4096-vindu, keep=4 — altså
   røk formatinstruksjonene først. Advarselen står i Ollamas logg, ikke i vår.

Kjøring 2 (qwen2.5:3b uten tankemodus, 16k kontekst) lukket funn 4 men traff samme
vegg: 600 s-taket er bindende på denne maskinvaren UANSETT modell — en 1B-modell
brukte også over ti minutter per kall mens CPU-en strupet seg til 54 %.

Ærlighetsgrense: null vellykkede modellkall. Prompt-former, VERDICT-linja og
runde-taket er fortsatt uverifiserte mot en levende modell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X737cbkG2uAXJ2Bvhf6X5M
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 20:12:50 +02:00
commit d2deb8ea38

View file

@ -0,0 +1,182 @@
# 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 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 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) |