Skip to content

Fix international model catalog aggregation - #110

Merged
maiphucgiang merged 3 commits into
mainfrom
fix/intl-model-catalog
Oct 31, 2026
Merged

maiphucgiang merged 3 commits into
mainfrom
fix/intl-model-catalog

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Use the personal /v2/enterprises/personal/models table as the primary catalog and supplement missing picker and account-root models from /v3/config.
  • Preserve personal declarations for duplicate model IDs and keep the existing fallback when the personal endpoint is unavailable.
  • Fix the WebUI regional model filter to use published profile metadata for automatic routes, so international models appear under the international filter.
  • Update the English and Chinese model-catalog documentation and add regression coverage for dual-path aggregation and regional filtering.

Verification

  • uv run --locked --no-build python -B tests/test_personal_models.py
  • for s in tests/test_*.py; do uv run --locked --no-build python -B "$s"; done
  • cd web && vp test run src/modelMetadata.test.tsx src/models.test.tsx src/components.test.tsx
  • cd web && vp check
  • cd web && vp build
  • git diff --check

Fixes #109

Summary by Sourcery

Improve international model catalog aggregation by combining personal and legacy catalogs, preserving partial results safely, and applying regional filters from published profile metadata.

New Features:

  • Supplement personal model catalogs with missing picker and account-root models from the legacy configuration catalog while preserving personal declarations for duplicates.

Bug Fixes:

  • Keep usable personal catalog data when supplemental catalog requests fail, while marking the refresh incomplete for retry.
  • Correct WebUI regional filtering to recognize regions from published profile metadata, including automatic routes.

Enhancements:

  • Add catalog completeness and fetch-sequence tracking to prevent partial or stale results from replacing valid cached catalogs.

Documentation:

  • Update English and Chinese catalog documentation to describe dual-source aggregation and merged catalog behavior.

Tests:

  • Add regression coverage for personal/v3 catalog merging, supplemental failures, cache completeness and ordering, and regional model filtering.

Use the personal model table as the primary catalog and supplement missing picker and account-root entries from /v3/config. Fix WebUI regional filtering to use published profile metadata for automatic routes.

Fixes #109
@sourcery-ai

sourcery-ai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR changes personal catalog loading to merge authoritative personal picker/account-root data with missing entries from /v3/config, preserving legacy fallbacks and personal duplicate declarations. It also corrects WebUI regional filtering for automatic routes by deriving regions from published profile metadata, with documentation and regression tests covering both behaviors.

Sequence diagram for personal model catalog aggregation

sequenceDiagram
    participant Caller
    participant Catalog as fetch_model_scopes
    participant Personal as fetch_personal_model_scopes
    participant Config as _fetch_config_model_scopes

    Caller->>Catalog: fetch_model_scopes(prefer_personal=true)
    Catalog->>Personal: fetch_personal_model_scopes()
    alt personal catalog available
        Personal-->>Catalog: personal picker and account scopes
        Catalog->>Config: _fetch_config_model_scopes()
        alt config catalog available
            Config-->>Catalog: legacy picker and account scopes
            Catalog->>Catalog: _merge_model_scopes(personal, legacy)
            Note over Catalog: Personal declarations win for duplicate IDs
        else config catalog unavailable
            Config-->>Catalog: RuntimeError or ValueError
            Catalog-->>Caller: personal scopes
        end
    else personal endpoint unavailable
        Personal-->>Catalog: _PersonalCatalogUnavailable
        Catalog->>Config: _fetch_config_model_scopes()
        Config-->>Catalog: legacy scopes
        Catalog-->>Caller: legacy scopes
    end
    Catalog-->>Caller: merged model scopes
Loading

Flow diagram for WebUI regional model filtering

flowchart LR
    Model[ModelRule] --> Explicit{region present?}
    Explicit -->|yes| Match[Compare with selected region]
    Explicit -->|no| Profile{profile present?}
    Profile -->|yes| Prefix[Use profile region prefix]
    Profile -->|no| Metadata[Inspect metadata_by_profile]
    Metadata --> Prefix
    Prefix --> Match
    Match --> Result[Include or exclude model]
Loading

File-Level Changes

Change Details Files
Make the personal model catalog authoritative while supplementing missing entries from the legacy configuration endpoint.
  • Extract the existing /v3/config fetch into a dedicated helper.
  • Merge picker and account-root scopes by model ID, preserving personal declarations for duplicates.
  • Retain the /v3/config fallback when the personal endpoint is unavailable or when supplemental retrieval fails.
app/credits.py
Document the dual-source catalog behavior and its scope-specific caching semantics.
  • Update English and Chinese documentation to describe supplementation, duplicate precedence, and merged models/serves caches.
docs/advanced.md
docs/advanced.zh-CN.md
Add regression coverage for dual-path aggregation and failure handling.
  • Verify missing models are imported from /v3/config while personal metadata wins on duplicate IDs.
  • Verify personal results remain usable when the supplemental endpoint returns an error.
tests/test_personal_models.py
Fix regional filtering for automatic routes by consulting published profile metadata.
  • Match explicit region fields and profile prefixes before falling back to metadata_by_profile keys.
  • Add UI coverage proving international and domestic-only models appear in their respective filters.
web/src/pages/Models.tsx
web/src/modelMetadata.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/credits.py" line_range="800" />
<code_context>
+            try:
+                legacy = _fetch_config_model_scopes(access_token, user_agent, domain=domain,
+                                                    uid=uid, enterprise_id=enterprise_id)
+            except (RuntimeError, ValueError):
+                # Personal data is already trusted; a supplemental catalog failure must not erase it.
+                return personal
</code_context>
<issue_to_address>
**Valid catalogs disappear on supplemental 401**

When the personal endpoint returns a valid catalog and `/v3/config` responds with HTTP 401, `fetch_model_scopes` does not catch `AuthExpiredError`, so `_sync_one_account_catalog` never publishes the valid personal catalog and a new account has no usable catalog.

Catch `AuthExpiredError` as a supplemental catalog failure and publish the trusted personal catalog.
</issue_to_address>

### Comment 2
<location path="app/credits.py" line_range="801-802" />
<code_context>
+                legacy = _fetch_config_model_scopes(access_token, user_agent, domain=domain,
+                                                    uid=uid, enterprise_id=enterprise_id)
+            except (RuntimeError, ValueError):
+                # Personal data is already trusted; a supplemental catalog failure must not erase it.
+                return personal
+            return _merge_model_scopes(personal, legacy)
+    return _fetch_config_model_scopes(access_token, user_agent, domain=domain,
</code_context>
<issue_to_address>
**Supplemental outages hide legacy models**

When the personal endpoint succeeds but the supplemental v3 request fails with a caught error during refresh, `fetch_model_scopes` returns personal-only scopes as a success, so `_sync_one_account_catalog` logs a successful refresh, replaces any merged catalog, and resets its freshness timestamp; operators miss the failure and legacy-only models remain unavailable until the next refresh, up to six hours later.

Surface supplemental failures and prevent partial scopes from being treated as fresh or replacing an existing merged catalog.
</issue_to_address>

### Comment 3
<location path="app/credits.py" line_range="759" />
<code_context>
+        rows = list(primary.get(scope) or [])
+        seen = {item.get("id") for item in rows if isinstance(item, dict) and item.get("id")}
+        rows.extend(item for item in supplemental.get(scope) or []
+                    if isinstance(item, dict) and item.get("id") not in seen)
+        merged[scope] = rows
+    return merged
</code_context>
<issue_to_address>
**Disabled models become discoverable**

When the personal catalog disables a model while `/v3/config` returns that ID as enabled, `_merge_model_scopes` builds `seen` after disabled personal rows have been removed, then appends the supplemental row, making the disabled model discoverable in the picker or account-root catalog.

Preserve disabled personal model IDs when filtering supplemental rows during the merge.
</issue_to_address>

### Comment 4
<location path="app/credits.py" line_range="798-803" />
<code_context>
+            personal = None
+        if personal is not None:
+            try:
+                legacy = _fetch_config_model_scopes(access_token, user_agent, domain=domain,
+                                                    uid=uid, enterprise_id=enterprise_id)
+            except (RuntimeError, ValueError):
+                # Personal data is already trusted; a supplemental catalog failure must not erase it.
+                return personal
+            return _merge_model_scopes(personal, legacy)
+    return _fetch_config_model_scopes(access_token, user_agent, domain=domain,
+                                      uid=uid, enterprise_id=enterprise_id)
</code_context>
<issue_to_address>
**Older refresh overwrites newer catalog**

When a background account refresh and a catalog scan overlap for the same account, and the older refresh completes last, the older request can finish its personal and v3 fetches after a newer refresh has stored its result, then return this older merged snapshot to the caller for storage; the cache replaces the newer catalog and resets its freshness time, so callers keep seeing stale models until the next refresh.

Track a per-account refresh sequence or fetched-at version and reject a result if a newer refresh has already been stored.
</issue_to_address>

### Comment 5
<location path="tests/test_personal_models.py" line_range="105" />
<code_context>
+        client = self._client(self.response(200, personal_payload()), self.response(503, {"code": 503}))
+        scopes = credits.fetch_model_scopes(
+            jwt("https://www.codebuddy.cn/auth/realms/copilot"), prefer_personal=True)
+        self.assertEqual([item["id"] for item in scopes["picker"]], ["auto", "hy4-preview"])
+        self.assertEqual(client.get.call_count, 2)
+
</code_context>
<issue_to_address>
**Personal root models escape the failure check**

When the supplemental v3 request fails, `test_personal_table_survives_supplemental_v3_failure` checks only `scopes["picker"]` on line 105, so it passes if the failure path preserves the picker but returns an empty `account` scope; callers then lose personal root models without this regression test failing.

Assert that the personal account scope is also preserved after the supplemental request fails.
</issue_to_address>

Linked issues

#109 webui中更新1.5.0后,国际版账号都搜不到模型了。 · 1 of 1 met

# Requirement Status Notes
1 国际版账号能搜到模型 ✅ Met Personal catalogs are supplemented with missing /v3/config models, and the WebUI’s international filter recognizes intl-* profile metadata.

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and v3-only model entries can be added to the personal account’s cached picker and routing catalogs. Reverting stops future merges, but already cached entries may remain until the catalog is refreshed; that cache is recomputable.

Blocking findings: app/credits.py:800, app/credits.py:802, app/credits.py:759, app/credits.py:803, tests/test_personal_models.py:105


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread app/credits.py Resolved
Comment thread app/credits.py Resolved
Comment thread app/credits.py
rows = list(primary.get(scope) or [])
seen = {item.get("id") for item in rows if isinstance(item, dict) and item.get("id")}
rows.extend(item for item in supplemental.get(scope) or []
if isinstance(item, dict) and item.get("id") not in seen)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Disabled models become discoverable

When the personal catalog disables a model while /v3/config returns that ID as enabled, _merge_model_scopes builds seen after disabled personal rows have been removed, then appends the supplemental row, making the disabled model discoverable in the picker or account-root catalog.

Preserve disabled personal model IDs when filtering supplemental rows during the merge.

Prompt for AI agents
In `app/credits.py` at line 759:

**Disabled models become discoverable**

When the personal catalog disables a model while `/v3/config` returns that ID as enabled, `_merge_model_scopes` builds `seen` after disabled personal rows have been removed, then appends the supplemental row, making the disabled model discoverable in the picker or account-root catalog.

Preserve disabled personal model IDs when filtering supplemental rows during the merge.

Comment thread app/credits.py Resolved
client = self._client(self.response(200, personal_payload()), self.response(503, {"code": 503}))
scopes = credits.fetch_model_scopes(
jwt("https://www.codebuddy.cn/auth/realms/copilot"), prefer_personal=True)
self.assertEqual([item["id"] for item in scopes["picker"]], ["auto", "hy4-preview"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Personal root models escape the failure check

When the supplemental v3 request fails, test_personal_table_survives_supplemental_v3_failure checks only scopes["picker"] on line 105, so it passes if the failure path preserves the picker but returns an empty account scope; callers then lose personal root models without this regression test failing.

Assert that the personal account scope is also preserved after the supplemental request fails.

Prompt for AI agents
In `tests/test_personal_models.py` at line 105:

**Personal root models escape the failure check**

When the supplemental v3 request fails, `test_personal_table_survives_supplemental_v3_failure` checks only `scopes["picker"]` on line 105, so it passes if the failure path preserves the picker but returns an empty `account` scope; callers then lose personal root models without this regression test failing.

Assert that the personal account scope is also preserved after the supplemental request fails.

Keep an already fetched personal catalog when the supplemental /v3/config request fails or rejects the token, record the result as incomplete so the refresh is retried, and never replace a complete cached catalog with an incomplete one.

Reject a catalog write that finishes after a newer refresh of the same account, and report supplemental failures in the manual scan and representative probe.

Cover the account scope, partial cache and stale sequence cases.
Remove unused imports and variables, drop needless f-strings, and rename ambiguous loop variables flagged by ruff across the gateway, tests, scripts and preview.

Keep the re-exported trial_rewards module and mark the deliberate sys.path-before-import test files.
@maiphucgiang
maiphucgiang merged commit fd78e7a into main Oct 31, 2026
6 checks passed
@maiphucgiang
maiphucgiang deleted the fix/intl-model-catalog branch October 31, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

webui中更新1.5.0后,国际版账号都搜不到模型了。

1 participant