fix(coach): say so when the model answered with reasoning alone

When every provider in the chain answered a Coach round with reasoning and
no reply, even after being asked once more without reasoning, the turn ended
with "could not reach a provider, please try again in a moment". The
provider was reached, and trying again in a moment changes nothing for a
model that cannot stop reasoning.

Such a chain now ends with its own code, coach.provider.reasoning_only, and
the drawer says the model only reasoned and never wrote an answer, and
suggests another model. The choice between credential_expired, rate_limited,
reasoning_only and unavailable moved into one function beside the other turn
error rules, where it is tested on its own. An un-wrapped reasoning-only
error maps the same way. Clients that do not know the code keep showing the
generic provider message.

Refs #1276
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 18:20:41 +02:00
1 parent c9673883b1
commit 31e107b96a
14 files changed
+144 -28

No files matched your search

@@ -817,6 +817,14 @@ describe("errorCodeToI18nKey", () => {
);
});
// #1276 — a model that only reasoned was reached; "try again in a
// moment" is the wrong advice for it.
it("maps coach.provider.reasoning_only to its own copy", () => {
expect(errorCodeToI18nKey("coach.provider.reasoning_only")).toBe(
"insights.coach.errorReasoningOnly",
);
});
// v1.39 — a refused turn names its reason. The capability refusals carry
// their code in `meta.errorCode`; each maps to copy that says what is off
// and who can change it, never the generic "provider unavailable".
@@ -87,6 +87,11 @@ export function errorCodeToI18nKey(code: string): string {
// the user to guided setup with a one-line explainer + a Settings
// link rendered alongside the bubble copy.
return "insights.coach.errorNoProvider";
case "coach.provider.reasoning_only":
// #1276 — the model was reached and answered with its reasoning
// only, even after being asked once more. "Try again in a moment"
// would be the wrong advice; the copy names what happened.
return "insights.coach.errorReasoningOnly";
case "coach.provider.unavailable":
case "coach.provider.empty":
case "coach.stream":