Skip to content

feat: unify provider discovery, model metadata, routing, and agent controls - #6302

Open
wangfei010313 wants to merge 42 commits into
agentscope-ai:mainfrom
wangfei010313:fix/provider-model-discovery
Open

feat: unify provider discovery, model metadata, routing, and agent controls#6302
wangfei010313 wants to merge 42 commits into
agentscope-ai:mainfrom
wangfei010313:fix/provider-model-discovery

Conversation

@wangfei010313

@wangfei010313 wangfei010313 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR unifies provider discovery, model metadata, model routing, and agent model controls.

It introduces a catalog-driven provider model system, runtime model discovery, capability-aware routing, fallback support, and a redesigned model selection experience. The implementation also hardens asynchronous I/O, persistence transactions, media formatting, and provider lifecycle handling.

The branch has been synchronized with the latest upstream main, and all merge conflicts in the provider/media implementation and related tests have been resolved.

What changed

Provider discovery and model metadata

  • Added a unified provider catalog and model catalog.
  • Added model capability metadata for context windows, multimodal support,
    thinking parameters, output limits, and model families.
  • Added provider-specific discovery policies.
  • Added runtime discovery for supported providers.
  • Added discovered-model persistence and reconciliation.
  • Added model availability and verification state handling.
  • Added catalog-only and authentication-aware discovery behavior.
  • Added support for custom, plugin, built-in, and discovered provider models.
  • Added protection against provider ID and model state conflicts.

Provider manager and persistence

  • Split provider discovery, persistence, catalog, availability, and plugin
    registry responsibilities into focused modules.
  • Added asynchronous provider initialization and discovery paths.
  • Moved synchronous file operations off the asyncio event loop.
  • Added transactional provider updates using detached snapshots.
  • Added atomic persistence and cancellation-safe state commits.
  • Added generation/revision checks for overlapping discovery operations.
  • Ensured provider SDK clients are closed on all discovery and probe paths.
  • Preserved in-memory and on-disk consistency when persistence is cancelled or
    fails.

Model routing and fallback

  • Added capability-aware model routing.
  • Added fallback model support across providers.
  • Added fallback request-state isolation for concurrent requests.
  • Added provider/model-specific thinking parameter handling.
  • Added support for model protocol overrides during discovery and connection
    testing.
  • Added model availability and verification results to provider APIs.

Agent model controls

  • Added agent-level model settings for fallback models, fallback policy,
    subagent models, and thinking level.
  • Added transactional agent configuration updates.
  • Prevented stale model settings from overwriting the active model.
  • Rejected explicit null for non-null routing fields while preserving
    subagent_model: null as a valid clear operation.
  • Added asynchronous configuration I/O for agent routes.

Media handling and formatters

  • Added asynchronous media preparation before downstream formatter calls.
  • Added local and remote media size limits.
  • Changed remote media downloads to bounded streaming reads.
  • Added oversized-media placeholder handling.
  • Preserved request-local media deduplication with ContextVar.
  • Added Anthropic video and HintBlock media handling.
  • Preserved provider-specific formatter behavior for OpenAI, Responses API,
    Anthropic, Gemini, and DashScope.
  • Added Responses API-specific image and video capability probing.
  • Added reasoning and disable_thinking handling for Responses models.

Console and model selector

  • Redesigned the model selector data and API layers.
  • Added provider/model capability display.
  • Added fallback and agent model settings UI.
  • Added candidate model management and model configuration editing.
  • Added recent, pinned, active, recommended, and discovered model states.
  • Added provider discovery and model synchronization feedback.
  • Added localized strings and focused frontend regression tests.

Packaging and runtime

  • Updated PyInstaller/Tauri packaging for provider catalogs and related data.
  • Updated desktop launch verification scripts.
  • Preserved cross-platform path and atomic file replacement behavior for
    Windows, macOS, and Linux.

Validation

Focused backend regression suites:

  • Model factory and media normalization tests
  • OpenAI Responses provider tests
  • Media capping tests
  • OpenAI provider tests
  • Provider manager tests

Result:

  • 259 passed
  • 1 skipped

Static checks for the changed conflict files:

  • Black: passed
  • Flake8: passed
  • Mypy: passed
  • Pylint: 10.00/10
  • Python compilation: passed
  • git diff --check: passed

Review follow-up

The branch has been rebased through a merge with the latest upstream main.
The following review concerns were explicitly addressed:

  • Cancellation-safe provider and agent persistence.
  • Runtime/disk state consistency after failed or cancelled writes.
  • SDK client cleanup for provider discovery and probing.
  • Bounded local and remote media processing.
  • Request-local formatter media state.
  • Responses API-specific capability probing.
  • Explicit-null validation for agent model settings.
  • Event-loop-safe configuration and media I/O.

@github-actions

Copy link
Copy Markdown

Welcome to QwenPaw! 🐾

Hi @wangfei010313, this is your 29th Pull Request.

🙌 Join Developer Community

Thanks so much for your contribution! We'd love to invite you to join the official QwenPaw developer group! You can find the Discord and DingTalk group links under the "Developer Community" section on our docs page:
https://qwenpaw.agentscope.io/docs/community

We truly appreciate your enthusiasm—and look forward to your future contributions! 😊

We'll review your PR soon.

@wangfei010313
wangfei010313 force-pushed the fix/provider-model-discovery branch from 29b31f7 to acf030d Compare July 22, 2026 12:29
@wangfei010313
wangfei010313 force-pushed the fix/provider-model-discovery branch from 7baefd1 to b57353c Compare July 27, 2026 06:48
- Restrict custom agent workspace_dir to home or WORKING_DIR
  (py/path-injection, high severity)
- Escape user-controlled values (agent/provider/model/plugin ids,
  session ids, skill names) with sanitize_log_value before logging
  across routers, providers, channels, and runtime (py/log-injection)
- Restore _evaluate_video_response delegation to the shared
  multimodal_prober evaluator lost in a merge-conflict resolution
- Re-align test_agents_ordering with the mutate_config transaction
  API and add the new supports_agent_thinking field to llamacpp
  expected metadata
- Drop dead assignment, unreachable guards, and duplicate config
  transaction path accumulated across review iterations
- Remove the superseded first dark-mode block and declarations fully
  overridden by the token restyle layer in ModelSelector styles; this
  also fixes stale orange/indigo colors leaking into dark mode
- Delete API type fields, interfaces, and store surface added during
  review iterations but never read (availability_*/discovery_* mirror
  fields, AgentSummary routing copies, EligibleProvider.chatModel,
  turnUsageStore.reset, unused triggerRef)
- Drop unreferenced sync methods superseded by their async
  counterparts (_merge_and_save_provider_snapshot, save_provider_config,
  clear_active_model, _save_plugin_provider, register_plugin_provider,
  PluginProviderRegistry.register)
- Deduplicate _replace_with_retry onto provider_persistence and drop
  the write_provider_snapshot replace_operation indirection
- Delete production code only reachable from tests (ComparisonSummary,
  generate_summary, _register hook, _sanitize_connection_message,
  identity _local_source_to_base64, ProbeSource enum, unused
  ModelConnectionResult re-export)
- Collapse duplicated discovery snapshot writers and the async
  identity wrapper in list_provider_info
- Build the default agent workspace path through safe_join so the
  caller-supplied custom id cannot influence the final path
  (py/path-injection, high)
- Sanitize agent ids and exception text in workspace, service
  manager, and multi-agent manager log entries (py/log-injection,
  26 pre-existing alerts swept into this PR's scan scope)
- Align _resolve_custom_workspace_dir with black formatting
- Drop the now-unused background_tasks parameter from
  _should_auto_discover
- Move sanitize_log_value imports after standard-library imports in
  service_manager and multimodal_prober
- Bridge fallback transparency data past agentscope's event
  conversion (which drops ChatResponse.metadata): FallbackChatModel
  publishes each hop into a request-scoped sink and
  QwenPawAgent._reasoning re-attaches it to outgoing events and the
  final assistant message, so Console and channel notices now fire
- Stop a broken mid-chain candidate (revoked key, deleted model)
  from masking healthy fallbacks behind it
- Skip fallback slots that fail to instantiate instead of breaking
  the primary model at agent build time
- Drop the provider_manager roundabout imports in the persistence
  and discovery mixins (SECRET_DIR and capability catalog are now
  imported from their source modules; tests patch those directly)
- Persist /model switch and reset off the event loop via
  update_agent_config_async; guard catalog temp-file cleanup on
  Windows so it cannot mask the original install error
- Cover the model-to-event chain and mid-chain skips with tests
The legacy providers.json migration read this module's SECRET_DIR
import, which the test-isolation fixtures (global conftest,
kimi/mimo tests) do not patch -- on a machine with a real legacy
file, tests would load real API keys and delete the user's file.
Derive the path from self.root_path.parent instead so instance
paths keep flowing from the single patched provider_manager
SECRET_DIR, and drop the second patch point added to the
provider-manager test fixture.
FallbackChatModel set its active-model ContextVar per attempt but never
reset it, so after a fallback the between-requests window (compaction
budget via context_size, capability learning via model_key) kept seeing
the last-served candidate instead of the primary the next request tries
first. Requests now snapshot the pre-request state and restore it when
the response settles, including the streaming wrapper's finally.

Remote media downloads raised on HTTP/network errors, which the model
error policy misclassified (404 -> model_not_found) and burned the
whole fallback chain on every turn while the dead URL stayed in
history. Failures now degrade to the same placeholder path as
unreadable local files, sibling downloads are shielded with
return_exceptions, and oversize chunked downloads no longer fabricate
an exact byte size.
The discovery and persistence mixins reference dozens of private
members defined in the other two files of the ProviderManager trio
with nothing declaring that surface, so a rename in one file breaks
the others silently. ProviderManagerHost now declares every cross-file
attribute and helper; both mixins inherit it, putting the stubs last
in the MRO so real implementations always win while static checkers
can verify each mixin in isolation. A contract test regenerates the
external-member set from the AST and fails when a mixin starts using
an undeclared member.

Also repairs UTF-8 text mangled by a GBK round-trip in provider
docstrings, a persistence log message, and a test docstring.
The backend-settings PATCH treated absent fields as explicit clears
(model_dump defaults), so sending only {model} silently erased a
stored reasoning_effort; it now applies model_fields_set semantics
like its sibling endpoint. The OAuth confirm modal gained an unmount
cleanup (polling used to outlive the component and could navigate
from a dead modal) and its five-minute timeout now reports an error
and closes instead of spinning forever. Adds the missing
models.discoveryOrigin*/availability*/modelAlreadyConfigured locale
keys and the new oauthTimeout message across en/zh/ja/ru/pt-BR/id.
An async provider update that lost the revision race left its stale
snapshot on disk, where it would swallow the concurrent winning update
on the next restart; the stale exit now rewrites the snapshot from the
live provider via _restore_latest_snapshot, shared with the discovery
path that already used this compensation inline.

The Windows fallback-kill reap replaced its 5s bound with an unbounded
proc.wait(), so a child stuck in uninterruptible kernel I/O could park
a worker thread forever; the wait is bounded again (leaked handle over
hung thread).

Startup sync failures were logged through an f-string, interpolating
the provider id and exception text directly (CodeQL log injection);
switched to lazy %s formatting with sanitized values.
Narrow the fallback stream token before handing it to the consumer
(mypy arg-type), let black settle the host-contract test formatting,
mark the mixins' intentionally-inherited host stubs for pylint's
abstract-method check, and clean the unused import and protected-access
noise in the new tests.
HarnessModelSelector was the only consumer of the backend-settings
PATCH and relied on the old clear-on-omit behavior; JSON.stringify
drops undefined, so under field-presence semantics its clears became
silent no-ops that diverged from the optimistic UI state. It now sends
explicit nulls, restoring the intended clears.

The OAuth modal guard reset on every open change, so parents that keep
the modal mounted lost the close protection: a poll response in flight
at close time could still fire onSuccess from a dismissed modal. The
guard now tracks liveness (!open), Continue ignores double-clicks,
bails when the modal closed during startOAuth, and clears any previous
timers before installing new ones.

FallbackChatModel._end_request now enforces the between-requests
invariant directly after the token reset: CPython silently restores an
outdated token's snapshot, so a stream abandoned in request 1 and
closed mid-request-2 would have reinstated the leaked fallback model.

_restore_latest_snapshot deletes the freshly written snapshot when the
provider was removed mid-flight instead of leaving a file that would
resurrect the provider on the next startup scan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants