Topic 1 (HITL): native HITL is GA (ctx.request_info/@response_handler/
run(responses=); GroupChatBuilder.with_request_info → AgentApprovalExecutor),
but durable checkpoint-resume is fragile (open #5818/#6127/#6372 into 1.9.0)
→ capture verdict out-of-band in VerdictStore, defer checkpointing off MVP path.
Topic 2 (MCP citation): REVERSES brief lean — official server-filesystem cannot
cite (raw text + bare paths) → build thin custom local-folder MCP server
returning {file,locator,snippet,score} over a framework-agnostic in-process
retriever (D7 seam). Corrected docs error: ContextProvider(source_id) +
before_run/after_run + extend_instructions(source_id,...) DO exist in 1.9.0.
Topic 3 (local chat client): use OpenAIChatCompletionClient(base_url) NON-STREAMING
(not OpenAIChatClient/Responses) — installed, 0 new deps, UsageDetails None-safe
and populated non-streaming. Native OllamaChatClient is --pre fallback (spike-gated).
validator-as-retry mitigates weak small-model tool-calling; Intel-CPU = plumbing only.
All grounded in installed 1.9.0 source (source wins over Learn docs). Gemini
bridge unavailable (MCP SDK predates Google May-2026 API change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fif1r1En5W542HbZV88yMH
21 KiB
| type | created | question | confidence | dimensions | mcp_servers_used | local_agents_used | external_agents_used | topic | brief | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| trekresearch-brief | 2026-06-24 | How does agent-framework-core 1.9.0 run agents against a local model on the free/local profile (OpenAI-compatible endpoint, Ollama, or other local chat client), and does that path populate UsageDetails token counts or return None? | 0.80 | 6 |
|
|
|
3 | .claude/projects/2026-06-24-fase2-mvp-vertical-slice/brief.md |
Real local-profile chat client for agent-framework 1.9.0 + UsageDetails
Generated by trekresearch (Voyage 5.6.0) on 2026-06-24. Topic 3 of 3 for the Fase 2 MVP-vertical-slice brief. Scope: external swarm + installed-source introspection (installed API truth wins). Gemini unavailable (MCP SDK broke on Google's May-2026 API change).
Research Question
How does agent-framework-core 1.9.0 (+ agent-framework-openai) run agents
against a LOCAL model on the free/local profile, and does that path populate
UsageDetails token counts or return None? Decision it feeds: the backend-profile
abstraction (local path), the budget middleware's None-handling, the end-to-end
run, and cost-discipline verification.
Executive Summary
Two local paths exist, and the brief's framing needs two corrections. Path A —
OpenAIChatCompletionClient(base_url=<local /v1>) — is installed (1.8.2), needs
zero new dependencies, and populates UsageDetails None-safely from the local
server's usage object. Path B — the native OllamaChatClient — is a --pre
beta package (agent-framework-ollama 1.0.0b260521) that is NOT installed and
adds the ollama client dependency. Correction 1 (client): use
OpenAIChatCompletionClient (Chat Completions API — the surface Ollama/LM Studio
/v1 exposes), NOT OpenAIChatClient (Responses API). Correction 2 (streaming):
run the debate/validator path NON-STREAMING — that simultaneously dodges the
well-documented /v1 streaming+tool-calling drop bugs AND the streaming
usage=None problem (UsageDetails is reliably populated only non-streaming).
Token accounting works (confirmed in installed source), so the "real token
accounting" success criterion is satisfiable on the local profile. The real limits
are model capability and Intel-Mac CPU speed: small CPU-runnable models are weak at
tool-calling, and an Intel Mac (no Metal/MLX, ~4–6 tok/s on 7B) makes a multi-round
debate take minutes — so local is for plumbing/smoke verification, with
representative-quality runs pushed to Foundry. Confidence 0.80.
Dimensions
1. Local chat-client paths — Confidence: high
Installed-source findings (ground truth):
agent-framework-openai1.8.2 exports bothOpenAIChatClient(Responses API) andOpenAIChatCompletionClient(Chat Completions API) — both import OK in the venv.RawOpenAIChatClient/RawOpenAIChatCompletionClientare the lower-level variants.- Both accept
base_url(→ envOPENAI_BASE_URL) andapi_key(required even for local — pass a dummy like"ollama"/"not-needed").model_idselects the model. - The native Ollama path:
agent_framework/ollama/__init__.pyire-exportsOllamaChatClient,OllamaChatOptions,OllamaEmbeddingClient, … fromagent_framework_ollama— butagent_framework_ollamais NOT installed (import agent_framework_ollama→ ModuleNotFoundError; only the lazy re-export stub exists). Installing it (uv add agent-framework-ollama --prerelease=allow) pullsollama >=0.5.3,<0.5.4.
External findings:
- Correct client for local
/v1: Ollama/LM Studio/vLLM expose a Chat Completions surface (/v1/chat/completions), soOpenAIChatCompletionClientis the technically correct client;OpenAIChatClienttargets the Responses API which local servers don't expose. Two official MS docs disagree on which to use for Ollama — flagged conflict; the Chat-Completions client is correct for/v1. https://learn.microsoft.com/agent-framework/integrations/openai-endpoints - Official local-endpoint table: Ollama
http://localhost:11434/v1/, LM Studiohttp://localhost:1234/v1/, vLLMhttp://localhost:8000/v1/. The/v1/suffix matters; the nativeOllamaChatClientuses:11434WITHOUT/v1. - Native
OllamaChatClientis preview/beta (1.0.0b260521, "4-Beta",--pre), wrapsollama.AsyncClient(native/api/chat),OTEL_PROVIDER_NAME='ollama'. MS docs describe it as having "full support for function tools and streaming." https://learn.microsoft.com/agent-framework/agents/providers/ollama
2. UsageDetails population — Confidence: high
Installed-source findings (the load-bearing answer):
OpenAIChatCompletionClient:_chat_completion_client.py:705—usage_details=self._parse_usage_from_openai(response.usage) if response.usage else None. None-safe._parse_usage_from_openai(:757-776) mapsusage.prompt_tokens→input_token_count,usage.completion_tokens→output_token_count,usage.total_tokens→total_token_count(+ reasoning/cached/audio details).UsageDetailsis the TypedDict inagent_framework/_types.py(input/output/total_token_count: int | None);add_usage_details(u1, u2)helper exists (_types.py:417) — use it to accumulate across calls in the shared budget meter. Lands onresponse.usage_details(_types.py:547); readresponse.usage_details["total_token_count"](None-safe).- Streaming:
_chat_completion_client.py:724-726—if chunk.usage: Content.from_usage(...)— usage only arrives if the provider emits a usage chunk.
External findings:
- Non-streaming: Ollama/LM Studio/llama.cpp
/v1all return a populatedusageobject →UsageDetailspopulated. Confirmed multi-source. https://docs.ollama.com/api/openai-compatibility - Streaming: OpenAI semantics require
stream_options={"include_usage": true}; whether MAF sets it automatically is unverified — so streamedUsageDetailsmay beNone. → run non-streaming for the accounting path. - Native
OllamaChatClient: maps Ollama'sprompt_eval_count→input,eval_count→output;total_token_countis NOT set (derive it). Streaming updates carry no usage. https://docs.ollama.com/api/usage - MAF usage plumbing is young: "Fix OTel usage detail attributes" landed in python-1.8.0 (already in our 1.9.0).
3. Tool-calling / structured output on local models — Confidence: high (load-bearing risk)
External findings (strong multi-source pattern):
- Small CPU-runnable models are markedly weak at tool-calling: 7–8B tool-selection F1 ≈ 0.48–0.57; failure modes = never calling the tool (answers from memory), emitting the tool call as plain text/JSON instead of a structured
tool_callsobject, and argument type drift ("5"vs5). https://www.docker.com/blog/local-llm-tool-calling-a-practical-evaluation/ - Qwen3-class is the most reliable small family (F1 ≈ 0.93 in the same eval); MAF docs explicitly bless
qwen3:4b/llama3.2for tools and warn "not all models support function calling."num_ctx ≥ 32kimproves reliability. - Streaming + tools on
/v1historically silently dropped/leaked tool calls (5+ repos; fixed 2025-05-28 but a 0.12.3 regression remains) — another reason to run non-streaming. https://ollama.com/blog/streaming-tool - Structured output:
response_format: type[BaseModel]is available on both local-path clients, but reliability is model-dependent. - MAF bug #1772:
OpenAIChatClient+ Ollama + ChatContext middleware inserts the system prompt twice — verify against 1.9.0; the native client may avoid it. https://github.com/microsoft/agent-framework/issues/1772
Design fit: our blocking Pydantic validator becomes the mitigation — it must retry/repair on parse failure, turning model unreliability into a bounded retry loop rather than silent corruption.
4. Intel-Mac CPU performance reality — Confidence: medium-high
External findings:
- Intel Macs run Ollama CPU-only (no Metal/MLX — Apple-Silicon-only). Reported ~4–6 tok/s on a 7B model (i9 2019); 3–8 tok/s CPU-only generally — "usable for testing, painful for actual work." https://localaimaster.com/blog/ollama-system-requirements
- A maker-checker debate (N agents × M rounds × tool round-trips × validator retries) at single-digit tok/s runs into minutes per candidate.
Conclusion: Intel-Mac local is a plumbing-verification + smoke-test environment (does the slice wire up, do tool calls fire, does the validator parse, does usage come back?), not an iterative/batch run environment — exactly matching D6 ("develop on local, verify minimally on Foundry"). Keep local runs tiny: 1 maker + 1 checker, ≤3 rounds, tiny synthetic data, hard token caps, a 3–4B model.
5. Determinism boundary — Confidence: high
- Local LLM output is non-deterministic even at temperature 0 / fixed seed (documented edge cases). This does not conflict with the success criterion iff the determinism criterion is scoped to the validator + Monte-Carlo (fixed seed + fixed candidate input → identical accept/reject + identical P10/P50/P90), NOT to the LLM that produces the candidate.
- Trap to avoid: never assert end-to-end determinism through the LLM. Golden tests pin the validator's inputs, not the LLM's outputs. Our architecture already separates "agents propose (non-deterministic) → validator decides (deterministic)", so the boundary is sound — make it explicit in the test design.
6. Security / no-egress posture — Confidence: high
External findings:
OpenAIChatCompletionClient(base_url=localhost)keeps inference on-box; the OpenAI SDK sends only tobase_urland never contactsapi.openai.com; a dummyapi_keyis safe. No new dependency (uses already-installedagent-framework-openai). This is the lower-supply-chain-cost path.- Native
agent-framework-ollamais official MS but beta and adds theollamaclient dep (pinned<0.5.4) — a supply-chain + GA-discipline cost. - Ollama daemon: bind
127.0.0.1(never0.0.0.0— it has NO auth by design, CVE-2025-63389); pin Ollama ≥ 0.17.1 (clears CVE-2024-37032 "Probllama" RCE, the 39719-22 set, and CVE-2026-7482 "Bleeding Llama" GGUF heap-read); keepOLLAMA_DEBUGunset (else prompts are written toserver.log). - Model-pull is explicit egress (downloads from a registry) — document it in provenance; not "silent" but real. Air-gap option: pre-stage model blobs offline. LM Studio persists chats/RAG locally by default (treat as data-at-rest).
- No native telemetry/phone-home from MAF client (instrumentation off by default), Ollama (no inference telemetry), or LM Studio (off by default).
External Knowledge
Best Practice
- Local wiring:
OpenAIChatCompletionClient(base_url="http://127.0.0.1:11434/v1/", api_key="ollama", model_id="qwen3:4b")(non-streaming); or nativeOllamaChatClient(host=..., model_id=...)after--preinstall. Cost: no native cost metric — derivecost = tokens × per-model price(local = 0); token usage on OTelgen_ai.client.token.usage.
Known Issues
- Streaming+tools
/v1drop/leak (run non-streaming);#1772double-system-prompt with OpenAIChatClient+Ollama+middleware (verify vs 1.9.0); native clienttotal_token_countnot set (derive);prompt_eval_counthistorically flaky for cached/large prompts. No community report exists of MAF Python against a local model on an Intel Mac with token usage — this scenario is under-documented; verify empirically.
Gemini Second Opinion
Unavailable (same MCP-SDK/Interactions-API failure as Topics 1–2). No independent Gemini triangulation; treat as absent, not negative.
Synthesis
The brief's premise — "run locally via OpenAIChatClient(base_url=...); UsageDetails
works because the local server returns OpenAI-style usage" — is directionally
right but wrong in two specifics that would fail success criteria on the very
profile we develop on. First, the Chat Completions client
(OpenAIChatCompletionClient), not the Responses client (OpenAIChatClient),
is the one that talks to a local /v1 endpoint and the one whose verified
_parse_usage_from_openai populates UsageDetails. Second, streaming is the
trap: it is exactly where local /v1 silently drops/leaks tool calls AND where
usage comes back None unless include_usage is set (uncertain in MAF). Running
the debate/validator path non-streaming fixes both at once — and non-streaming
is what we want anyway for deterministic accounting.
The contrarian's "use the native OllamaChatClient instead" is a real option (MS
docs route you to it; it sidesteps /v1 translation edges and gets
prompt_eval_count/eval_count directly), but it conflicts with the project's
GA-pin dependency discipline (it is a --pre beta + a new ollama dep). The
resolution is a spike-gate: start with the zero-new-dep Chat-Completions
base_url path non-streaming; if a quick spike shows tool-calls leaking as text, or
usage=None, or the #1772 middleware bug biting, switch to the native client and
accept the scoped --pre dependency. Either way the model-map → chat-client ctor
abstraction (D2) hides the choice behind the profile.
The deeper truth the swarm surfaces: on Intel-Mac CPU, "local primary" means local proves the slice RUNS (plumbing, tool-calls fire, validator parses, usage populated) — it does not prove the agents produce good candidates. "Both profiles exercised" therefore means: local = it runs; one minimal Foundry pass (cheapest model, hard cap) = the candidates are substantively sound. And the model's tool-calling weakness is absorbed by our blocking validator-as-retry — the obligatory validator stops being just a gate and becomes the reliability mechanism that makes a weak local model usable.
Open Questions
- Concrete local model + endpoint (brief
[OPEN], operator-supplied): default toqwen3:4bvia Ollama on127.0.0.1:11434for dev/smoke;qwen2.5:7b/qwen3:8bfor reliability checks. The whole free-local run hinges on a working local endpoint existing on the machine. - Foundry deployment names (brief
[OPEN], tenant-specific, operator-supplied) — back the role→deployment map for the minimal Azure/Foundry-profile check. - base_url vs native client — resolve via a one-call spike in /trekexecute:
does
OpenAIChatCompletionClient(base_url)non-streaming return populatedUsageDetailsand structured tool-calls on the chosen model? If yes, no new dep; if no, switch to nativeOllamaChatClient. - Does MAF set
include_usageon streaming? Unverified — irrelevant if we run non-streaming, but confirm if streaming is ever needed for narration.
Recommendation
Run the local profile via OpenAIChatCompletionClient(base_url=<local /v1>),
non-streaming, behind the D2 model-map abstraction; keep the native OllamaChatClient
as a spike-gated fallback.
- Client:
OpenAIChatCompletionClient(base_url="http://127.0.0.1:11434/v1/", api_key="ollama", model_id="qwen3:4b")— installed, zero new deps, honours the GA-pin discipline. NOTOpenAIChatClient(Responses API). - Non-streaming for debate + validator — dodges
/v1streaming+tools drop AND streamingusage=Nonein one move. Streaming only for non-tool narration, if ever. - Budget middleware: treat
usage_details is Noneas a HARD FAIL in dev (assertion) so a usage-reporting regression can never silently disable the budget cap. Maps directly to the "real token accounting" success criterion (assert the meter is populated fromUsageDetailson a real run). Accumulate viaadd_usage_details(...). Nolen(...split())proxy insrc/. - Validator-as-retry: the blocking Pydantic validator must retry/repair on parse failure — small models emit unparseable/text-leaked tool calls; this turns the obligatory validator into the reliability mechanism.
- Model:
qwen3:4bfor dev/smoke (MAF-blessed, tool-capable, CPU-tolerable);qwen2.5:7b/qwen3:8bfor reliability checks;num_ctx=32k. - Determinism golden tests: pin validator + Monte-Carlo inputs (fixed seed → identical decision/percentiles); never assert determinism through the LLM.
- "Both profiles exercised": local (full run) proves the slice RUNS; one minimal Foundry pass (cheapest model, hard token cap, per D6) proves candidates are substantively sound. Keep local runs tiny (1 maker + 1 checker, ≤3 rounds).
- Spike-gate the client choice in /trekexecute: if
base_urlnon-streaming shows tool-call text-leakage,usage=None, or #1772, switch to nativeOllamaChatClient(accept the--preagent-framework-ollamadep + verify itsprompt_eval_count/eval_countmapping; derivetotal_token_count). - Security hardening: Ollama bind
127.0.0.1, pin ≥ 0.17.1,OLLAMA_DEBUGunset; document model-pull as explicit egress; dummyapi_key.
Risks to carry into the plan: (a) Intel-CPU latency → local is plumbing/smoke, not
fast iteration; (b) small-model tool-calling weakness → lean on validator-retry +
Qwen3; (c) native-client fallback is --pre (GA-discipline tension) — adopt only if
spike-gated.
Sources
| # | Source | Type | Quality | Used in |
|---|---|---|---|---|
| 1 | .venv/.../agent_framework_openai/__init__.py:33-43 (OpenAIChatClient, OpenAIChatCompletionClient) |
codebase | high | Dim 1 |
| 2 | .venv/.../agent_framework_openai/_chat_completion_client.py:705,724-726,757-776 (UsageDetails mapping, None-safe) |
codebase | high | Dim 2 |
| 3 | .venv/.../agent_framework/_types.py:402-417,547 (UsageDetails, add_usage_details, response.usage_details) |
codebase | high | Dim 2 |
| 4 | .venv/.../agent_framework_openai/_chat_client.py:376,382 (base_url, api_key) |
codebase | high | Dim 1 |
| 5 | agent_framework/ollama/__init__.pyi + import agent_framework_ollama fails (not installed) |
codebase | high | Dim 1, 6 |
| 6 | https://learn.microsoft.com/agent-framework/integrations/openai-endpoints (base_url table, ChatCompletion client) | official | high | Dim 1, 2 |
| 7 | https://learn.microsoft.com/agent-framework/agents/providers/ollama (native client, tool caveat, qwen3:4b) | official | high | Dim 1,3 |
| 8 | https://learn.microsoft.com/python/api/agent-framework-core/agent_framework.usagedetails?view=agent-framework-python-latest | official | high | Dim 2 |
| 9 | https://pypi.org/project/agent-framework-ollama/ (1.0.0b260521 beta, --pre) | official | high | Dim 1, 6 |
| 10 | https://github.com/microsoft/agent-framework/blob/main/python/packages/ollama/pyproject.toml (deps: ollama <0.5.4) | official | high | Dim 1, 6 |
| 11 | https://docs.ollama.com/api/openai-compatibility (usage; streaming needs include_usage) | official | high | Dim 2 |
| 12 | https://docs.ollama.com/api/usage (prompt_eval_count/eval_count) | official | high | Dim 2 |
| 13 | https://github.com/microsoft/agent-framework/releases (1.8.0 OTel usage fix; 1.9.0 ollama tools fix) | official | high | Dim 2 |
| 14 | https://github.com/microsoft/agent-framework/issues/1772 (double system prompt, Ollama+middleware) | community | medium | Dim 3 |
| 15 | https://ollama.com/blog/streaming-tool (streaming+tools fixed 2025-05-28) | official | high | Dim 3 |
| 16 | https://github.com/ollama/ollama/issues/12557 (streaming tool-call regression 0.12.3) | community | medium | Dim 3 |
| 17 | https://www.docker.com/blog/local-llm-tool-calling-a-practical-evaluation/ (tool-call F1 by model) | community | high | Dim 3 |
| 18 | https://www.morphllm.com/best-ollama-models (Qwen3 best tool-calling) | community | medium | Dim 3 |
| 19 | https://localaimaster.com/blog/ollama-system-requirements (Intel i9 4-6 tok/s, no MLX) | community | medium | Dim 4 |
| 20 | https://github.com/ollama/ollama/issues/586 (seed+temp0 not deterministic) | community | medium | Dim 5 |
| 21 | https://www.wiz.io/blog/probllama-ollama-vulnerability-cve-2024-37032 (CVE-2024-37032) | community | high | Dim 6 |
| 22 | https://github.com/advisories/GHSA-f6mr-38g8-39rg (CVE-2025-63389 no-auth by design) | official | high | Dim 6 |
| 23 | https://www.runzero.com/blog/ollama/ (CVE-2026-7482 Bleeding Llama, < 0.17.1) | community | high | Dim 6 |
| 24 | https://www.indusface.com/blog/exposed-ollama-servers-llm-security-risks/ (bind 127.0.0.1) | community | high | Dim 6 |
| 25 | https://learn.microsoft.com/agent-framework/agents/observability (no native cost metric) | official | high | Best Practice |
| 26 | https://lmstudio.ai/app-privacy (LM Studio local persistence, no telemetry default) | official | medium | Dim 6 |