Commit Graph
2 Commits
Author SHA1 Message Date
cd9add7a27 fix(feishu): surface permission errors clearly and keep users on page (#3032)
* fix(feishu): surface permission errors clearly and keep users on page

Map Feishu/Lark API failures to typed OpenViking errors with actionable hints, and keep Web Studio from treating HTTP 403 permission denials as session logout.

* fix(feishu): simplify API error mapping

* refactor(feishu): inline API error mapping

---------

Co-authored-by: wugj <wugj@g-bits.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
2026-07-07 19:55:43 +08:00
dingben be77b2dac1 feat: implement multi-credential priority call design (#2468)
* feat: implement multi-credential priority call design

Add OrderedCredentialSwitcher for N-credential failover, MultiCredentialVLM and FailoverEmbedder for credential switching, support automatic migration from legacy backup config

* refactor: deduplicate AllCredentialsFailedError and fix logging in switcher

* chore: fix code formatting for lint compliance

* chore: fix remaining code formatting

* fix: update FailoverEmbedder for multimodal API compatibility

* fix

* fix

* refactor: split error classes and fix fail-fast in credential failover

Separate the monolithic PERMANENT error class so the switcher reacts to
the actual root cause:

- 400 (request-level parameter error) -> PERMANENT, fail-fast: same
  request fails on every credential of the same model.
- 401/403/unauthorized/accountoverdue -> new AUTH class: credential-level,
  advances to the next credential in multi-credential mode.
- new CONTENT_SAFETY class (moderation rejections) and INPUT_TOO_LARGE ->
  fail-fast: switching credentials cannot help.

classify_api_error now checks CONTENT_SAFETY before PERMANENT so a
moderation message containing "400" is not misclassified. On fail-fast the
failover wrappers re-raise the original exception (preserving type/info)
instead of wrapping it, so callers can react (e.g. truncate on
input_too_large). AllCredentialsFailedError is reserved for chain
exhaustion. The legacy PrimaryBackupSwitcher also switches on AUTH to keep
existing backup behavior.

* fix: make get_active_index side-effect free in credential switcher

get_active_index() previously mutated state: when a failback threshold was
met it would decrement the active index. Because observability properties
(active_credential_index / active_credential_id) call it, merely reading the
current credential for logging or metrics could accidentally advance the
failback state machine.

Split the concern: get_active_index() is now a pure read, and a new public
maybe_failback() performs the one-step failback (and logs an info line when
the active credential index changes). The request loops in MultiCredentialVLM
and FailoverEmbedder call maybe_failback() at the top of each attempt, so the
failback behavior is unchanged while pure reads no longer have side effects.

* fix: drop global total_max_retries cap from credential failover

The failover loops exited on `idx >= n OR total_attempts >= total_max_retries`
(default 10). With more than 10 credentials, or when failback churn inflated
the attempt count, this could raise AllCredentialsFailedError before every
credential had actually been tried, leaving lower-priority credentials unused.

Remove total_max_retries entirely from MultiCredentialVLM and FailoverEmbedder:
credential exhaustion is now decided solely by reaching the end of the chain
(idx >= n), and per-credential retries remain the responsibility of each
underlying instance via its own max_retries. The aggregated error tuple now
records the failing credential index instead of the attempt counter.

Adds a regression test covering more than 10 credentials all being tried.

* refactor: move model-behavior fields off EmbeddingCredential

encoding_format, model_path, cache_dir, enable_fusion, res_level and
max_video_frames describe how a model runs, not which credential is used.
All credentials of a single embedding model share the same model, so these
belong on the parent EmbeddingModelConfig, not on each credential.

Keeping them on the credential forced a `cred.X or config.X` merge in
_create_failover_embedder, which silently dropped explicit falsy values
(enable_fusion=False, res_level=0, max_video_frames=0) and fell back to the
parent value.

Remove these six fields from EmbeddingCredential and read them directly from
the parent config when building per-credential embedders. id/provider/model/
api_key/api_base/api_version/ak/sk/region/host/extra_headers remain
credential-level.

* fix: raise instead of guessing dimension in FailoverEmbedder

FailoverEmbedder.get_dimension() returned a hardcoded 2048 when the first
embedder had no get_dimension(). That path is reached only when wrapping
sparse embedders, which have no fixed dense dimension; returning a fabricated
2048 silently feeds a wrong dimension to callers (e.g. schema creation).

Delegate to the first embedder and raise AttributeError when it has no
get_dimension(), surfacing the misuse instead of hiding it.

* fix: token usage aggregation in failover wrappers

Two issues in the cross-instance token usage merge:

1. Encapsulation: FailoverVLM / MultiCredentialVLM / FailoverEmbedder reached
   into other instances' private _token_tracker. Add a public token_tracker
   accessor on VLMBase and use it in the VLM mergers.

2. Double counting in FailoverEmbedder: embedders share a process-wide
   singleton token tracker (_get_token_tracker), so all wrapped embedders point
   at the same object. Merging N identical trackers inflated usage N-fold.
   Return a single instance's usage directly instead of merging.

* fix: trip circuit breaker on AUTH errors after error-class split

Splitting 401/403/unauthorized/accountoverdue out of PERMANENT into the new
AUTH class (commit b477b9fd) regressed the circuit breaker: it only tripped
immediately on PERMANENT/QUOTA_EXCEEDED, so auth errors no longer opened the
breaker right away.

For a single embedding instance an auth failure (key invalid / no permission /
overdue) is persistent and retrying is pointless, so the breaker should still
trip immediately. Add ERROR_CLASS_AUTH to the immediate-trip set and update the
classification tests to assert the new AUTH class (403 still trips the breaker).

* fix

* test: bump _last_switch_time when forcing active_idx in ring tests

Without setting _last_switch_time, maybe_failback() retreats to idx 0
immediately because the default 0 timestamp is always older than the
600s timeout, so the unavailable last credential never actually gets
exercised.

* format

* fix

* fix: add dimension valid

* format

* fix: resolve VLM legacy backup primary via _match_provider()

When a legacy config uses ``providers: {openai: {api_key: ...}}`` together
with a ``backup`` VLMConfig, the previous backup-migration branch only
read top-level ``self.provider/self.api_key`` to build legacy-primary,
yielding (provider=None, api_key=None) and an unavailable VLMConfig.

Both primary and backup migration now go through _match_provider() so
``providers``/``default_provider`` based legacy configs are migrated
into VLMCredential with the correct provider/api_key/api_base/etc.

Add regression tests covering primary-providers-dict + backup,
backup-providers-dict, default_provider on backup, and propagation of
extra fields.

* refactor: drop misleading wrapper.is_exhausted from failover wrappers

The ring-retry rewrite of MultiCredentialVLM / FailoverEmbedder does not
call OrderedCredentialSwitcher.on_failure(); each request loops locally
and raises AllCredentialsFailedError on full failure. As a result the
underlying _active_idx is rarely advanced to n, so wrapper-level
is_exhausted would have returned False even when every credential just
failed.

There are no production callers of either property, so remove them
(YAGNI) rather than synthesize an exhausted state from the wrapper side.
The switcher's own is_exhausted stays as a state-machine observation
point used by tests.
2026-06-15 15:37:03 +08:00