mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
* fix(daemon): let pi and opencode reach custom providers (MUL-6471) A self-host setup whose pi/opencode providers point at an OpenAI/Anthropic-compatible gateway could not run a Multica task on either runtime, while the same work succeeded when the CLI was invoked by hand in the same container (GH #7300). Gateway-style providers register model ids that themselves contain a slash — `claude/claude-opus-5` under provider `multica-anthropic` — and both failures came from treating that slash as a provider boundary, or from having no provider at all. pi: buildPiArgs split the model id on the first slash and passed the leading segment as --provider, so a slash-shaped id became `--provider claude --model claude-opus-5`. pi answers an unknown --provider with a hard error before it ever looks at --model. The selector now goes to --model whole and --provider is never synthesized: pi's own resolver accepts `provider/id`, a bare id, and an id containing a slash, so passing less is strictly more capable. Verified against pi 0.67.2 — the old argv reproduces `Unknown provider "claude"`, the new one resolves to provider=multica-anthropic, model=claude/claude-opus-5. opencode: --model is `provider/model`, and an id persisted without its provider is rejected outright (`UnknownError`, reproduced against opencode 1.18.14). The daemon cannot invent the missing segment, so it asks the runtime's own catalog: an id that exactly one provider claims is promoted to the catalog's canonical selector, and anything uncertain — a static fallback catalog, an already-canonical id, two providers exposing the same bare id — is passed through untouched for the CLI to judge. Two reported side effects fall out of the same root cause. `thinking_level` was dropped for both runtimes because the capability lookup is keyed on the model id and missed every catalog entry; it now matches. And `starting agent` logged entry.Model, the env-var tier alone, so every task whose model came from agent.model announced itself with an empty model — the resolution now happens before the log. The task row's `model: null` is not part of this: it is read from the run's usage rows, which a run that fails before its first provider call never produces. Co-authored-by: multica-agent <[email protected]> * fix(daemon): gate selector qualification on the runtimes that can use it Review follow-up on MUL-6471. The first pass ran catalog discovery for every task that pinned a model, on the argument that the path already takes one memoized lookup for thinking_level / service_tier. That argument does not hold: cachedDiscovery deliberately skips the cache for empty and fallback results (#3729, MUL-5549), precisely so a transient failure can retry. A logged-out or failing runtime would therefore pay its full discovery ceiling — 15s for opencode/pi/omp, 30s for openclaw — on every task, including runtimes whose bare model ids could never be rewritten anyway. Qualification is now an explicit runtime capability. ModelIDsAreProviderQualified is true only where Model.Provider is literally the ID's own prefix: pi builds `provider + "/" + id`, opencode reads the provider back off the id. Runtimes carrying a bare id next to a display-name Provider — claude's "anthropic", copilot's "openai", dsh's "DeepSeek" against a `deepseek-official/...` id — never reach discovery. Built-in runtime identities resolve through their protocol family, so a pi-family fork like omp inherits the capability from its descriptor rather than a name check. The daemon side moves into qualifyTaskModel so the call boundary is testable. TestQualifyTaskModelSelectorsAndDiscoveryBoundary pins both halves: pi, opencode and omp resolve to the selector their CLI accepts, and claude, codex and an empty model never reach listModels at all. TestModelIDsAreProviderQualifiedMatchesCatalogShape keeps the capability honest by deriving the expected answer from the real catalog producers instead of restating the list — it fails if the predicate and the emitted ID shape ever disagree. Co-authored-by: multica-agent <[email protected]> * fix(daemon): read a runtime's model catalog at most once per task Second review round on MUL-6471. Two findings, one root cause: the daemon treated catalog discovery as cheap when cachedDiscovery deliberately makes it anything but — an empty or fallback result is never memoized (#3729, MUL-5549), so every read costs a logged-out or timing-out runtime its full 15-30s ceiling again. A single task could pay that twice. qualifyTaskModel read the catalog to promote the model selector, then ValidateThinkingLevel read it again to check the level — back to back, on exactly the reporter's configuration (pi or opencode plus thinking_level=high). The two now share one lazily-loaded, task-scoped catalog: resolveTaskModelSelection owns it, and the validators take that loader through ValidateThinkingLevelWith / ValidateServiceTierWith rather than reaching for ListModels themselves. The global immediate-retry contract is untouched — the reuse lives and dies with the task. The second finding removes the read entirely for pi. Qualification is not a launch requirement there: buildPiArgs already proves pi's resolver takes a canonical selector, a bare id, and an id containing a slash, so a pi task with no capability override now starts without spawning a discovery subprocess at all. ModelIDsAreProviderQualified is replaced by ModelSelectorMustBeProviderQualified, which states the contract that actually justifies the cost — the CLI refuses an unprefixed id — and is therefore true only for opencode and its DevEco fork. That also retires the catalog-shape claim the old predicate made, which deveco was already a counterexample to. The resulting matrix, pinned by TestResolveTaskModelSelectionReadsTheCatalogAtMostOnce: pi/opencode with a thinking level qualify and validate on one read; pi, omp, claude and unpinned-model tasks read nothing; opencode without an override still reads, because it cannot launch otherwise; codex without a model still fails both checks closed without reading. A failed read is not retried inside the task. Verified the count assertion bites by removing the memoization and watching the pi and opencode cases report 2 reads. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]>