mateclaw/mateclaw-server/src/main/java/vip/mate/llm/chatmodel/ChatModelBuilder.java
matevip 3b11a3def6 fix(failover): AUTH_ERROR triggers fallback chain + UI splits provider 401 from session expiry
Two related issues from the Kimi-401 user report:

1. Backend (NodeStreamingChatHelper): a primary AUTH_ERROR (e.g. Kimi 401
   with an invalid API key) returned immediately without trying the
   fallback chain — a fallback provider with a different, valid key
   never got a chance. Even with DashScope correctly configured as the
   fallback, the user chat dead-ended on a 401.

   The original assumption ("auth never self-heals so do not retry")
   holds for the primary same-model retry loop but is wrong for the
   fallback chain — different providers have different keys. Apply the
   same break-into-fallback policy that BILLING and MODEL_NOT_FOUND
   already use. recordPrimary(false) is preserved so the cooldown
   counter still accumulates.

2. Frontend (chatError.ts + i18n): the error-text matching for
   /认证|auth|unauthorized|401/i was so broad it matched the substring
   "auth" inside URLs like https://api.kimi.com/.../auth, classifying
   any model 401 as user "session expired" and rendering the misleading
   "页面将自动跳转到登录页" copy. (The redirect itself only fires from
   /api/v1/auth/* axios paths and SSE-connection 401s, not from this
   payload-text path — but the copy alone is the worst kind of false
   alarm.)

   Add a new ChatErrorCategory provider_auth_error and split the
   pattern matching: narrow auth_expired (HTTP 401 / 登录已过期 /
   session expired / 凭证失效) is matched FIRST, then the broad
   401-ish pattern routes to provider_auth_error. BACKEND_ERROR_TYPE_MAP
   for AUTH_ERROR is also remapped, since structured backend payloads
   currently always come from LLM providers — never from our own
   /api/v1/auth path.

Tests
- NodeStreamingChatHelperFailoverTest (5 cases): primary 401 →
  fallback succeeds; chain skips auth-failing fallback to next healthy
  one; whole-chain failure surfaces last AUTH_ERROR (no silent drop);
  BILLING regression unchanged; primary-success path does not touch
  chain
- Browser preview verified: new i18n keys resolve in en-US, classifier
  correctly routes "[错误] 401 from kimi.com" → provider_auth_error
  while "[错误] HTTP 401 from /api/v1/auth/ping" stays auth_expired
- 186 tests pass (was 181 + 5 new); vue-tsc clean

Do-not-touch list: handleAuthFailure() in useStream/api/index.ts (real
session-expiry path) is unmodified — only the misclassification
upstream is fixed. auth_expired i18n copy is unchanged.
2026-04-19 17:45:15 +08:00

46 lines
2.1 KiB
Java

package vip.mate.llm.chatmodel;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.retry.support.RetryTemplate;
import vip.mate.llm.model.ModelConfigEntity;
import vip.mate.llm.model.ModelProtocol;
import vip.mate.llm.model.ModelProviderEntity;
/**
* Strategy contract for assembling a Spring AI {@link ChatModel} from a
* persisted {@link ModelConfigEntity} + {@link ModelProviderEntity} pair.
*
* <p>One implementation per {@link ModelProtocol}. New provider protocols
* are added by registering a new {@code @Component} that implements this
* interface — no edits to {@link ProviderChatModelFactory} or
* {@code AgentGraphBuilder} required.</p>
*
* <p>This interface lives in {@code vip.mate.llm.chatmodel} (under the {@code llm}
* package) so that {@code llm.failover.ProviderInitProbe} can build a
* {@link ChatModel} for health-probing without depending on the {@code agent}
* package — which would create the circular dependency
* {@code agent → llm → agent}. Prior to this extraction, all model-building
* logic lived inside {@code AgentGraphBuilder}.</p>
*/
public interface ChatModelBuilder {
/** The protocol this builder handles; the factory routes by this key. */
ModelProtocol supportedProtocol();
/**
* Build a fresh {@link ChatModel} for the given runtime configuration.
* Implementations must be stateless — callers may invoke this many times
* for the same model id and expect equivalent (but not necessarily ==)
* results.
*
* @param model runtime model row with temperature / max tokens / etc.
* @param provider provider row supplying API key, base URL, and provider-level
* generate kwargs
* @param retry retry template to wire into the underlying Spring AI client
* where supported. Implementations whose protocols don't
* expose a Spring AI {@code RetryTemplate} hook (DashScope
* native, ChatGPT Responses) may ignore this parameter.
*/
ChatModel build(ModelConfigEntity model, ModelProviderEntity provider, RetryTemplate retry);
}