Files
majordomo/progress.md
T
steveandClaude Opus 5 02cd561eaf
Gadfly review (reusable) / review (pull_request) Successful in 5m14s
Adversarial Review (Gadfly) / review (pull_request) Successful in 5m14s
CI / Tidy (pull_request) Successful in 9m24s
CI / Build & Test (pull_request) Successful in 9m53s
feat(qwen): Alibaba Qwen built-in over Model Studio's OpenAI-compatible mode
Adds the `qwen` built-in provider and the `qwen://` DSN scheme, keyed by
QWEN_API_KEY and defaulting to Model Studio's international host. Like kimi
(ADR-0026) it is `provider/openai` pointed elsewhere — no new client.

Model Studio serves the same models over two protocols, so the real decision
was which wire format to speak. ADR-0027 records why it is the OpenAI one:
down the anthropic client `ReasoningEffort` is ignored by design, structured
output rides the first-party `output_config.format` mechanism the shim does
not implement, and cached-token accounting reads Anthropic-only usage fields.
Each of those fails silently rather than loudly, which is what makes the
choice worth writing down. The shim stays reachable ad hoc via an
`anthropic://` DSN.

The kimi and qwen DSN factories were byte-identical, so they now share one
`openaiCompatScheme` helper: the "credential comes from the DSN token, and
the missing-key hint names LLM_<NAME>" rules hold by construction instead of
by copy.

Tests are hermetic and break-checked (all six fail on a deliberate mutation),
including the reverse credential leak — a visible QWEN_API_KEY must not
authenticate the openai built-in — and reasoning_effort asserted on the wire
body, which is the ADR's load-bearing claim.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-12 16:07:41 -04:00

317 lines
17 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.
# progress
## 2026-06-27 — llama-swaps (TLS) DSN scheme
Follow-up to the llama-swap provider: added the `llama-swaps` DSN scheme (https
base URL) alongside `llama-swap` (http, local-first), mirroring redis/rediss, so
a TLS-fronted instance is first-class instead of being pushed to the `openai://`
scheme. Scheme-only (no default built-in); shares one factory in builtin.go.
## 2026-06-27 — llama-swap provider + canonical image-gen interface
**Landed (ADR-0015, ADR-0016).** New `provider/llamaswap`: chat **delegates to
`provider/openai`** at `{base}/v1` (no duplicated wire client per ADR-0007), with
legacy `max_tokens`, a `Bearer no-key` placeholder for keyless local instances,
and a timeout-free client (swap cold starts → use context deadlines). Tailored
management methods on the concrete type — `ListModels`, `Running` (raw JSON),
`Unload`. DSN scheme `llama-swap://token@host:port` builds an **http** base URL
(local-first), registered in `builtin.go` alongside a no-URL built-in that errors
on use (mirrors foreman).
New canonical `imagegen` package (text-to-image), separate from `llm`:
`Request`/`Result`/`Model`/`Provider`, `Image = llm.ImagePart` so generated
images feed back into chat. First backend is llama-swap via OpenAI
`/v1/images/generations` (`b64_json`, bytes-only). Re-exported from root
(`ImageModel`, `ImageRequest`, `WithImageSize`, ...). v1 is txt2img only; edits/
img2img and registry image-DSN resolution deferred.
Hermetic `httptest` tests for chat delegation, management endpoints, image
decode, and scheme wiring. Gates green. README support matrix + image-gen
section, CLAUDE.md package map, and ADR index updated in the same change.
## 2026-06-10 — Phase 9b: mort converted, PR open
**Done.** mort fully re-based on majordomo on branch
`majordomo-conversion`: 230 files (+8726/6211), go-llm/v2 and
go-agentkit removed from go.mod/go.sum with a clean repo-wide grep,
`go build`/`go vet` clean, full test suite green (80 packages ok, 0
failures). Highlights: pkg/logic/llms rebuilt as the choke point
(registry, lane decorators, convar tier resolver, failover wiring via
llms.Wire); skillexec/agentexec on majordomo agent loops (critic budget
via WithMaxStepsFunc, steer, compactor, tool-error guards); runDirect
special case deleted; scaddy critic redesigned as one-shot multimodal
Generate; agentkit httpapi replaced by a mort-side server; ~96 tools on
DefineTool. PR (open, not merged):
https://gitea.stevedudenhoeffer.com/steve/mort/pulls/1274
Run note: executed by an 8-agent staged workflow; one mid-run deadlock
(a cluster agent polling a long-tail package) was broken by converting
tasks/recipe/summary/cookbook in the main line; one full workflow restart
after a network outage.
## 2026-06-10 — Phase 9a: conversion-driven library extensions
**Landed (ADR-0014):** RegisterResolver (dynamic DB-backed tiers, static
aliases win, recursive + cycle-guarded), DefineTool[Args] (typed tools
over SchemaFor), Usage cache/reasoning detail fields populated by
anthropic/openai/google, WithPromptCaching (Anthropic top-level
cache_control), agent hooks (WithMaxStepsFunc, WithSteer, WithCompactor —
non-fatal on error, canonical transcript stays uncompacted —
WithToolErrorLimits with ErrToolLoop), health Bench/Unbench/Snapshot,
ChainConfig.Observer failover events (attempt/bench/skip). Full hermetic
coverage for each.
**Next:** Phase 9b — the mort conversion branch.
## 2026-06-10 — Phase 8: live validation against real Ollama Cloud
**All six checks PASS** (examples/live harness, OLLAMA_API_KEY from .env):
1. Tier aliases (`thinking` = minimax-m3:cloud→kimi-k2.6:cloud,
`workhorse` = minimax-m2.7:cloud→qwen3-coder:480b-cloud) resolve via
Parse, incl. as a trailing chain element.
2. Plain chat served by ollama-cloud/minimax-m3:cloud (189 in/48 out).
3. Live tool call: the workhorse agent actually invoked get_launch_code
and answered from its result in 2 steps.
4. Structured Generate[T] decoded {City:Tokyo Country:Japan
Population:14000000 Latitude:35.6762}.
5. Forced failover: an unreachable head (connection refused = transient)
was retried, benched, and fell through to a live cloud tail; the second
request skipped the benched head without dialing it.
6. Agent with the calc skill attached invoked calculate and answered
56161.
**Discovery + fix:** Ollama Cloud ignores the `format` field entirely
(verified with raw curl — markdown came back despite a schema). The
ollama provider now also states the schema as an explicit system
instruction (local stays constrained-decoded; cloud becomes
instruction-guided); hermetic test added. The `:cloud`-suffixed model
names work verbatim against ollama.com — mort's tier strings carry over
unchanged.
**Next:** Phase 9 — convert mort onto majordomo, open the PR.
## 2026-06-10 — Phase 7: examples, migration blueprint, README finalization
**Landed:** `examples/` — nine runnable programs, one per hard requirement
(parse, failover incl. trailing-alias chains, custom tiers, LLM_* env
providers + foreman, multimodal, raw tool loop, structured Generate[T],
agent with toolbox, skills) + examples/README index; all built by the
hermetic gate suite. `docs/mort-migration.md` — the full conversion
blueprint: layering (what stays mort-side), the symbol-level core
mappings table, seven planned additive library extensions (dynamic
resolvers, DefineTool[Args], usage detail fields, prompt caching, agent
loop hooks, manual bench controls, failover observer), the Phase 9
execution order, and the behavioral deltas to verify (failover knob
mapping, AdvanceOnPermanent for go-llm's ErrRequestSpecific behavior,
bytes-only images). README final pass with the complete feature/provider
matrix.
**Next:** Phase 8 — live validation against real Ollama Cloud.
## 2026-06-10 — Phase 6: skills
**Landed:** `skill/` (ADR-0013): the agent.Skill contract satisfied by a
buildable skill.New(name, WithInstructions/WithTools/WithToolbox);
instruction-only skills legal; same-instance reuse across agents; additive
ordered composition proven (prompt appending + toolset merge + loud
duplicate policy). Example skills: `skill/clock` (time_now/time_convert,
injectable clock) and `skill/calc` (calculate over a hand-rolled
recursive-descent evaluator: + - * / % ^, parens, unary minus, scientific
notation; division-by-zero and non-finite results rejected). Tests cover
the evaluator table, tool execution through ExecuteTool, and a full
agent-loop run answering from the calculate result.
**Next:** Phase 7 — examples/, mort migration map, README finalization.
## 2026-06-10 — Phase 5: agent loop, Generate[T], schema derivation
**Landed:** `agent/` (ADR-0012): New(model, system, opts) with toolboxes,
max steps (default 10), per-step request options, agent-level observers +
per-run OnStep, WithHistory continuation (Result.Messages round-trips),
sequential tool dispatch through panic-recovering ExecuteTool, unknown
tools → IsError results, duplicate tool names fail loudly, partial Result
preserved on ErrMaxSteps/model errors/cancellation. The agent.Skill
interface ships here (instructions + tools composition is tested with a
stub); the skill package with real implementations is Phase 6.
`llm.SchemaFor[T]` reflect-derived strict-compatible JSON schemas
(pointers→nullable anyOf, description/enum tags, maps/slices/time/RawMessage,
recursion rejected) and root `majordomo.Generate[T]` (schema injection,
fence-stripping decode, model-naming errors). 15 agent tests + schema +
Generate suites, all hermetic.
**Next:** Phase 6 — skill package + two example skills.
## 2026-06-10 — Phase 4: Google provider (official genai SDK)
**Landed:** `provider/google` on google.golang.org/genai v1.59.0 (ADR-0011):
lazy cached client (construction never fails; missing key = synthetic 401
so chains fail over), assistant→model role mapping, FunctionResponse tool
results with output/error payloads, ParametersJsonSchema raw-schema tools,
ResponseJsonSchema structured output, ToolChoice→FunctionCallingConfig,
ReasoningEffort→ThinkingConfig.ThinkingLevel, usage includes thought
tokens, iter.Pull2-adapted streaming, genai.APIError→llm.APIError mapping.
Hermetic tests via HTTPOptions.BaseURL + httptest (SSE fixtures for
streaming). Registry: google + gemini schemes wired to the real provider;
the last stub machinery deleted — all six built-ins are now real clients.
README matrix: Google row fully ✅.
**Next:** Phase 5 — Agent run loop, Toolbox ergonomics, Generate[T].
## 2026-06-10 — Phase 3: REST providers (OpenAI, Anthropic, Ollama×3) + media
**Landed:**
- `provider/openai`: Chat Completions client for OpenAI and every
OpenAI-compatible endpoint (tools with string-arguments mapping, strict
SSE streaming incl. by-index tool-call assembly and the empty-choices
usage chunk, response_format json_schema, max_completion_tokens with a
WithLegacyMaxTokens compat option, reasoning_effort).
- `provider/anthropic`: Messages API client (anthropic-version 2023-06-01,
required-max_tokens defaulting, tool_use/tool_result blocks with native
is_error, GA structured output via output_config.format, full SSE event
parser with input_json_delta buffering, 529-overloaded classified
transient, usage sums cache tokens).
- `provider/ollama`: ONE native /api/chat client serving ollama (local,
OLLAMA_HOST normalization), ollama-cloud (https://ollama.com + bearer
OLLAMA_API_KEY), and foreman (base URL + bearer; tolerates its
buffered-single-object "streaming"). Object tool arguments, tool_name
results, format-schema structured output, think-level mapping, NDJSON
streaming with 16MB lines.
- `media/`: normalization pipeline per ADR-0009 (magic-byte sniffing,
box-filter downscale, transcode preference ladder, byte-budget quality
ladder, webp passthrough-or-reject, copy-on-write, everything-unfittable
wraps ErrUnsupported).
- Chain executor now normalizes media PER TARGET before each attempt and
advances penalty-free past targets that can't take the request (proven:
text-only head + vision fallback; per-target downscale assertions).
- Registry: real providers + scheme factories wired for openai, anthropic,
ollama, ollama-cloud, foreman (google still stubbed, Phase 4);
WithHTTPClient registry option; required env-foreman TLS chat round-trip
test (LLM_FM=foreman://token@host → Parse("fm/qwen3:30b") → bearer
arrives, chat answers).
- ADR-0009 (multimodal), ADR-0010 (tools/structured mapping); README
matrix flipped to ✅ for the four landed provider families; ~70 new
hermetic tests across the three provider packages + media.
- Run note: openai/anthropic/media were built by three parallel
subagents against the frozen llm contract; ollama/foreman, chain wiring,
and registry integration done in the main line. All gates green.
**Next:** Phase 4 — Google provider on google.golang.org/genai.
## 2026-06-10 — Phase 2: health + failover chain, proven
**Landed:** the full deterministic failover test matrix over the fake
provider + fake clock (no sleeps, no network): single-transient recovery
via same-target retry; repeated transients bench + advance; cooldown expiry
re-admits and success resets; backoff doubling across bench rounds;
mixed chain with an inline-expanded alias element failing over through the
expanded targets; permanent-policy default (fail-fast on auth) and
`AdvanceOnPermanent` override; `TransientRetries` disabled/custom; retry
loop stops early when the tracker benches mid-request; exhaustion error
lists skipped-while-benched targets; custom classifier override; chain-of-
one gets identical semantics; HTTP 529 fails over. Implementation needed no
changes — Phase 1's executor held up.
**Next:** Phase 3 — OpenAI/Anthropic/Ollama/foreman REST clients + media
pipeline.
## 2026-06-10 — Phase 1: foundations, ADRs, skeleton, docs
**Landed:**
- Module scaffold (Go 1.26), `.gitea/workflows/ci.yaml` (foreman-style
gates: build, vet, race tests, tidy-diff), `.env.example`.
- `llm/` canonical contract: Message/Part (sealed; text+image),
Request/Options, Response/Usage/FinishReason, Stream/StreamEvent,
Tool/Toolbox (panic-safe Execute), Capabilities (zero-value semantics),
Model/Provider interfaces, APIError + transient/permanent Classify.
- `health/`: clock-injected tracker — consecutive-failure threshold,
exponential capped cooldown, reset-on-success, thread-safe; full
deterministic test suite (fake clock).
- Root: Registry (providers/aliases/schemes/health), Parse with the binding
grammar (verbatim model ids, inline recursive alias expansion, cycle
detection, dedup), LLM_* env-DSN loading (go-llm-parity lazy fallback +
eager LoadEnv/New scan), chain executor implementing Model
(retry-on-transient, bench-on-repeat, skip-benched, 404-advance,
fail-fast-on-auth, joined exhaustion errors). Built-ins register as
resolvable stubs until their phases land.
- `provider/fake/`: scriptable provider (per-model outcome queues, request
recording, capabilities overrides, streaming) — the hermetic test rig.
- ADRs 00010008 + index; CLAUDE.md; honest README with pending-marked
matrix.
- Tests cover the two required cases: the trailing-`thinking` chain parse
and `LLM_M1=foreman://token@host` loading (plus DSN table, lazy fallback,
cycle detection, chain failover/backoff/exhaustion, toolbox execution,
error classification).
**Notes:** chain executor landed in Phase 1 (design was settled);
Phase 2 deepens its test matrix (cooldown re-admission via fake clock,
alias-in-chain failover, permanent-policy override) and wires anything the
tests flush out.
**Next:** Phase 2 — exhaustive health/chain test matrix.
## 2026-07-11 — audio surfaces + image editing (ADR-0017, ADR-0018)
- New leaf package `audio`: `SpeechModel`/`SpeechProvider` (TTS) and
`TranscriptionModel`/`TranscriptionProvider` (STT), imagegen conventions
(zero value = backend default, options + Apply, bytes in/out). Root
re-exports added.
- `imagegen.Editor` — optional img2img interface (`EditRequest` with
`Init Image` + `Strength`); shared `Result`.
- provider/llamaswap implements all of it: `/v1/audio/speech`,
`/v1/audio/transcriptions` (multipart), `ListVoices`
(`/v1/audio/voices?model=`, shape-tolerant), `/sdapi/v1/img2img`
(txt2img wire + init_images/denoising_strength, shared decode), and a
cheap `Health(ctx)` probe (GET /health) for often-offline hosts.
- Hermetic httptest coverage for every new wire shape + validation errors.
- Consumer: mort's llamaswap media tool cluster (status/image/TTS/STT tools).
## 2026-07-18 — Kimi (Moonshot AI) built-in provider (ADR-0026)
- New built-in `kimi` provider + `kimi://` DSN scheme: Moonshot's
OpenAI-compatible Chat Completions endpoint, so both reuse `provider/openai`
(no new client, mirrors llama-swap's chat path). Default base URL
`https://api.moonshot.ai/v1`; China endpoint via
`LLM_KCN=kimi://[email protected]/v1`.
- Credential is `KIMI_API_KEY` (read through the registry's injected envLookup,
so it's hermetically testable). `WithAPIKey` is passed unconditionally so an
unset `KIMI_API_KEY` can never fall through to the openai client's
`OPENAI_API_KEY` default.
- New `openai.WithAPIKeyName` option customizes the missing-key error hint
(default `OPENAI_API_KEY`); the kimi built-in/scheme name `KIMI_API_KEY`.
- Hermetic tests (capturing RoundTripper): built-in base URL + bearer, missing
key names KIMI_API_KEY with no OPENAI fallthrough and no network hit, and the
kimi:// scheme round-trips against the China host.
- Docs kept in sync: README built-in table + DSN scheme list + support matrix
(footnote ³), `.env.example`, ADR-0026 (+ index; also backfilled the missing
0024/0025 index rows).
- Consumer: mort names Kimi as a failover tier.
## 2026-08-12 — Qwen (Alibaba) built-in provider (ADR-0027)
- New built-in `qwen` provider + `qwen://` DSN scheme over Alibaba Model
Studio's OpenAI-compatible mode, reusing `provider/openai` (no new client,
mirrors kimi/llama-swap). Default base URL
`https://dashscope-intl.aliyuncs.com/compatible-mode/v1`; China/regional
hosts via `LLM_QCN=qwen://[email protected]/compatible-mode/v1`.
- Credential is `QWEN_API_KEY` (via the registry's injected envLookup).
`WithAPIKey` passed unconditionally so an unset key cannot fall through to
`OPENAI_API_KEY`; `WithAPIKeyName` names `QWEN_API_KEY` in the 401 hint.
- **Chose OpenAI-compat over Model Studio's Anthropic-compatible
`/apps/anthropic` shim** (ADR-0027): on the anthropic client
`ReasoningEffort` is ignored by design, `Request.Schema` rides
`output_config.format` (which the shim does not implement), and cached-token
accounting reads Anthropic-only usage fields. The shim is still reachable
ad hoc via an `anthropic://` DSN.
- kimi and qwen DSN factories were byte-identical, so they now share one
`openaiCompatScheme` helper — the credential + key-hint rules come by
construction, not by copy.
- Hermetic tests: built-in base URL + bearer, missing key names QWEN_API_KEY
with no OPENAI fallthrough and no network hit, the reverse leak (a visible
QWEN_API_KEY must not authenticate the openai built-in), `qwen://` round-trip
against the China host, its keyless hint naming LLM_QCN, and
`reasoning_effort` asserted on the wire body (the ADR's load-bearing claim).
captureRT now records the request body; all six break-checked.
- Docs in sync: README built-in table + Qwen paragraph + DSN scheme list +
support matrix (footnote ⁴), `.env.example`, ADR-0027 (+ index).
- Consumer: mort wants Qwen as a failover tier.