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]>
317 lines
17 KiB
Markdown
317 lines
17 KiB
Markdown
# 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 0001–0008 + 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.
|