Skip to content

Model capability metadata, preflight guard, international shared catalogs and image merge - #32

Merged
maiphucgiang merged 2 commits into
mainfrom
feat/model-capabilities-intl-images
Sep 16, 2026
Merged

maiphucgiang merged 2 commits into
mainfrom
feat/model-capabilities-intl-images

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Changes

  • Publish safe upstream catalog metadata on /v1/models and /admin/models: capability states, token limits and per-profile declarations with catalog_source and safe source_variants; account identities, credentials and internal configuration are never exposed. The WebUI model page shows full details and shared-catalog sources; declarations are read-only references, not native-modality or measured guarantees.
  • Add model_capability_guard, default on, configurable via CLI, CODEBUDDY2API_MODEL_CAPABILITY_GUARD and a WebUI hot switch. Each request is preflighted against its effective routing candidates: declared-unsupported images, tools or reasoning, reasoning efforts outside the declared options and output caps above the declared limit return 400 without an upstream call. Disabling it only stops preflight; authentication, image counts and sizes, capacity and strict bindings stay enforced.
  • Deduplicate international CLI/WorkBuddy catalogs into a shared view. Only enabled, catalog-ready international accounts contribute; a target account's own declarations win and missing IDs inherit shared ones with source tracking. Conflicts resolve to the higher known rate, smaller limits and effort intersection; unknown rates never mean free. Model rejections isolate by (account, product, model) instead of endpoint-wide international blocks; domestic catalogs, balances, bindings and auto defaults remain independent.
  • Merge lossless consecutive user messages containing images for international profiles only (Images are silently dropped when a user message is followed by another user message (HTTP 200, prompt collapses) #25): all text, images, order and tool boundaries are preserved, with no retries or fabricated replies. Domestic backends keep their native behavior.

Verification

  • All 55 backend test scripts pass, including new international-catalog, capability and message-normalization suites plus extended routing, site-block and WebUI tests; 130 frontend tests and 14 browser regressions at 1440/375 px pass.
  • Deployed this code to the actual local 8787 instance with its existing credentials and data. All 65 advertised models carry metadata; deepseek-v4.1-flash appears on intl-cli as a shared inherited entry at rate 0 and answers through the intl-cli account.
  • Live guard contrast at zero cost: with the guard on, a glm-5.0 image request returns 400 unsupported_image_input and a hy3 request above its declared output cap returns 400 model_output_limit; after the hot toggle off, the identical hy3 request succeeds with credit 0, and the guard is restored. Local rejections are audited without upstream calls.
  • International consecutive-user image requests repeatedly answer correctly through intl-cli at credit 0; domestic hy3-x requests on cn-work bill normally (0.01 and 0.05 credits). Domestic no-merge behavior is covered offline; live routing prefers zero-rate international accounts.
  • Runtime settings were restored after validation; model rules and bindings are unchanged. No dependency or version changes (1.2.6).

Rollback

Disable model_capability_guard to stop preflight for new requests. Shared catalogs and international image merging hold no persisted state; reverting this commit restores previous routing. Before downgrading source, remove the new startup option and use a control-store backup without the new persisted setting.

Summary by Sourcery

Add safe model capability metadata, configurable request preflight, shared international catalogs, and lossless international image-message normalization while preserving routing and security boundaries.

New Features:

  • Expose safe per-profile model capability metadata, limits, catalog provenance, and shared source variants through model APIs and the WebUI.
  • Add configurable model capability preflight validation with CLI, environment, management, and WebUI controls.
  • Provide deduplicated shared catalogs for international profiles while preserving native declarations, account boundaries, and source tracking.
  • Merge consecutive image-bearing user messages for international profiles without altering domestic request behavior.

Bug Fixes:

  • Isolate international model rejection cooldowns by account, product, and model instead of blocking an entire endpoint.
  • Prevent capability-incompatible requests from reaching upstream services while retaining existing authentication, binding, capacity, and size protections.

Enhancements:

  • Use conservative conflict resolution for inherited international metadata, including stricter limits and intersected reasoning options.
  • Expand routing previews, audit records, documentation, frontend model details, and settings UI to represent the new metadata and request behavior.

Build:

  • Forward the model capability guard environment setting through Docker Compose.

Documentation:

  • Document model declarations, capability preflight behavior, shared international catalogs, image-message normalization, and rollback considerations in English and Chinese guides.

Tests:

  • Add backend, frontend, and browser coverage for capability metadata, preflight validation, international catalog sharing, message normalization, account-isolated model blocks, and WebUI controls.

… merge intl image messages

- /v1/models and /admin/models now publish safe catalog capabilities, limits and per-profile declarations; WebUI shows full model details and shared-catalog sources.
- New model_capability_guard (default on; CLI/env/WebUI hot switch) rejects declared-unsupported images, tools, reasoning options and output limits before dispatch; disabling it only stops preflight.
- International CLI and WorkBuddy use a deduplicated shared catalog view with readiness gating, source tracking, conflict resolution (higher rate, smaller limits, effort intersection) and per-account model-block isolation; domestic catalogs, balances and bindings stay independent.
- International profiles merge lossless consecutive user messages containing images (fixes image loss reported in #25); domestic backends keep native behaviour. No new retries or replays.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai

sourcery-ai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds safe profile-specific model capability metadata and WebUI presentation, a default-on preflight that rejects explicit unsupported request requirements before upstream dispatch, a readiness-gated conservative shared catalog for international accounts with account-scoped model backoff, and lossless image-message merging for international routes.

Sequence diagram for capability preflight and international image normalization

sequenceDiagram
    participant Client
    participant Gateway
    participant CredentialPool
    participant Catalog
    participant Upstream

    Client->>Gateway: POST /v1/chat/completions
    Gateway->>Gateway: Requirements.from_request(body, payload, protocol)
    Gateway->>CredentialPool: headers_for(..., requirements=requirements)
    CredentialPool->>Catalog: entry_model(gateway, entry, model)
    Catalog-->>CredentialPool: Effective model declaration
    alt Explicit capability violation
        CredentialPool-->>Gateway: capability_error(...)
        Gateway-->>Client: 400 preflight error
    else Candidate is compatible
        CredentialPool-->>Gateway: Credential and headers
        Gateway->>Gateway: merge_intl_user_images(body, profile)
        Gateway->>Upstream: Routed request
        Upstream-->>Gateway: Response
        Gateway-->>Client: Response
    end
Loading

Entity relationship diagram for model declarations and profile variants

erDiagram
    MODEL ||--o{ PROFILE_DECLARATION : has
    PROFILE_DECLARATION ||--o| CATALOG_SOURCE : identifies
    CATALOG_SOURCE ||--o{ SOURCE_VARIANT : preserves

    MODEL {
        string id
        object capabilities
        object limits
    }
    PROFILE_DECLARATION {
        string profile
        object safe_metadata
    }
    CATALOG_SOURCE {
        string kind
        string[] profiles
    }
    SOURCE_VARIANT {
        string profile
        object metadata
    }
Loading

State diagram for model capability guard behavior

stateDiagram-v2
    [*] --> Enabled
    Enabled --> Preflight: New request
    Preflight --> Rejected: Declared mismatch
    Rejected --> Client400: capability_error
    Preflight --> InternationalNormalize: Compatible candidate
    InternationalNormalize --> Dispatch: intl-cli or intl-work
    Preflight --> Dispatch: Domestic profile
    Dispatch --> UpstreamResponse
    Enabled --> Disabled: WebUI or configuration switch
    Disabled --> Dispatch: New request
    Disabled --> InternationalNormalize: International route
    Disabled --> Enabled: Guard re-enabled
Loading

File-Level Changes

Change Details Files
Publishes sanitized, profile-aware model capability metadata through public and administrative model APIs and renders it in the WebUI.
  • Allowlist and sanitize model declaration fields, nested reasoning/context data, URLs, and source provenance.
  • Aggregate capabilities and token limits as supported, unsupported, mixed, or unknown without exposing account or credential data.
  • Add model detail panels, profile/source-variant views, capability badges, display-name search, and route-preview metadata.
app/model_capabilities.py
app/model_policy.py
app/admin_api.py
app/gateway_management.py
converter.py
web/src/api.ts
web/src/modelMetadata.tsx
web/src/pages/Models.tsx
web/src/pages/Settings.tsx
web/src/ui.module.scss
tests/test_model_capabilities.py
tests/test_webui_integration.py
web/src/modelMetadata.test.tsx
web/e2e/model-capabilities.spec.ts
Adds request-time capability preflight that filters only eligible routed candidates before upstream dispatch.
  • Detect image, tool, reasoning, thinking-disable, and mapped output-limit requirements across supported protocols.
  • Reject explicit known incompatibilities with structured 400 errors while allowing unknown declarations and preserving existing authorization, binding, capacity, and size checks.
  • Add default-on configuration through CLI, environment, persisted/WebUI settings, request-context snapshots, audit events, and lease/capability rechecks.
app/model_capabilities.py
app/request_context.py
app/settings.py
converter.py
app/admin_api.py
audit_store.py
docker-compose.yml
docs/advanced.md
docs/advanced.zh-CN.md
docs/webui.md
docs/webui.zh-CN.md
tests/test_environment_config.py
tests/test_model_capabilities.py
tests/test_webui_integration.py
Builds a readiness-gated shared international catalog while retaining account ownership and conservative conflict resolution.
  • Share only enabled accounts with ready, isolated catalogs; prefer target-native declarations and inherit missing international models.
  • Merge inherited prices, limits, modality flags, and reasoning options conservatively, retaining safe provenance and source variants.
  • Keep domestic catalogs, balances, bindings, auto defaults, and account authority independent; scope model rejection backoff by international account, product, and model.
app/model_catalog_view.py
converter.py
app/model_policy.py
app/gateway_management.py
tests/test_intl_catalog.py
tests/test_model_site_blocks.py
tests/test_region_routing.py
docs/advanced.md
docs/advanced.zh-CN.md
Normalizes consecutive image-bearing international user messages after routing without changing domestic requests or retry behavior.
  • Merge only contiguous user runs with compatible message attributes, preserving text, image order, tool boundaries, and original input immutability.
  • Reject unmergeable content or conflicting attributes, recheck request size, release leases on failure, and record merge audit counters.
  • Use the canonical unmerged body for international-to-domestic failover.
app/message_normalization.py
converter.py
audit_store.py
tests/test_message_normalization.py

Assessment against linked issues

Issue Objective Addressed Explanation
#25 Prevent images from being silently discarded when consecutive user messages contain image parts, including images in the first, middle, or final message of the consecutive-user run.
#25 Apply the image-loss workaround only to the affected international backends while preserving message content/order, image data, message attributes, and system/assistant/tool boundaries.
#25 Reject consecutive image-bearing user runs that cannot be safely represented instead of forwarding a request that could silently lose image content, while retaining normal size checks and avoiding retries or mutation of the caller's body.

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-16T21:55:43.522927Z fafb8ab PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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 2 issues

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

## Individual Comments

### Comment 1
<location path="app/model_catalog_view.py" line_range="97-98" />
<code_context>
+        if isinstance(model, dict) and model.get("id") and (model_id is None or model["id"] == model_id):
+            result.setdefault(model["id"], model)
+    shared = {}
+    for source_profile, models in sources:
+        if source_profile not in INTERNATIONAL:
+            continue
+        for model in models or []:
+            name = model.get("id")
+            if (not name or name in result or name in _AUTOMATIC or model.get("disabled")
+                    or (model_id is not None and name != model_id) or model.get("supportsToolCall") is not True):
+                continue
+            variants = shared.setdefault(name, [])
+            record = (source_profile, model)
+            if record not in variants:
+                variants.append(record)
+    for name in sorted(shared):
+        result[name] = _common_model(shared[name])
+    return list(result.values())
</code_context>
<issue_to_address>
**issue (bug_risk):** The shared catalog combines only source entries whose `supportsToolCall` is exactly `True`, so an international model declared for chat/image use but without a tool-call flag is silently excluded from inheritance and cannot be routed through the other international product.

**Triggers:** When an international source catalog contains a usable model whose declaration omits or sets `supportsToolCall` to false.

**Suggested fix:** Do not use tool support as the eligibility gate for shared catalog inheritance; apply the actual model usability/product criteria and let capability preflight handle tool incompatibility.

```suggestion
            if (not name or name in result or name in _AUTOMATIC or model.get("disabled")
                    or (model_id is not None and name != model_id)): 
```
</issue_to_address>

### Comment 2
<location path="app/model_catalog_view.py" line_range="37-80" />
<code_context>
+def _common_model(sources):
</code_context>
<issue_to_address>
**issue (broader_impact):** The merged declaration starts as a deep copy of the first source model and leaves non-conflict-resolved fields unchanged, so conflicting descriptions, vendor metadata, parameter suggestions, or other allowlisted fields are presented as the effective shared declaration from whichever source happens to come first.

**Triggers:** When multiple international source accounts provide the same inherited model with different non-core metadata.

**Suggested fix:** Remove or explicitly reconcile every field that can differ, or omit conflicting fields from the effective shared declaration while retaining the originals in `source_variants`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and this changes model authorization and routing across international accounts, adds preflight rejection decisions, and rewrites image-bearing requests before sending them upstream. If the shared catalog or capability declarations are wrong, requests could be rejected or sent to an unsuitable external account/model and the resulting exposure or failed call cannot be undone by reverting, though the affected scope is bounded and the routing policy can be disabled.

Blocking findings: app/model_catalog_view.py:98, app/model_catalog_view.py:80


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

Comment thread app/model_catalog_view.py
Comment on lines +97 to +98
if (not name or name in result or name in _AUTOMATIC or model.get("disabled")
or (model_id is not None and name != model_id) or model.get("supportsToolCall") is not True):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): The shared catalog combines only source entries whose supportsToolCall is exactly True, so an international model declared for chat/image use but without a tool-call flag is silently excluded from inheritance and cannot be routed through the other international product.

Triggers: When an international source catalog contains a usable model whose declaration omits or sets supportsToolCall to false.

Suggested fix: Do not use tool support as the eligibility gate for shared catalog inheritance; apply the actual model usability/product criteria and let capability preflight handle tool incompatibility.

Suggested change
if (not name or name in result or name in _AUTOMATIC or model.get("disabled")
or (model_id is not None and name != model_id) or model.get("supportsToolCall") is not True):
if (not name or name in result or name in _AUTOMATIC or model.get("disabled")
or (model_id is not None and name != model_id)):

Comment thread app/model_catalog_view.py
Comment on lines +37 to +80
def _common_model(sources):
models = [model for _, model in sources]
result = deepcopy(models[0])
for field in ("supportsImages", "supportsToolCall", "supportsReasoning", "canDisableThinking",
"supportsExtra", "disabledMultimodal", "onlyReasoning"):
value = _flag([model.get(field) for model in models],
restrictive=field in ("disabledMultimodal", "onlyReasoning"))
if value is None:
result.pop(field, None)
else:
result[field] = value
for field in ("maxInputTokens", "maxOutputTokens", "maxAllowedSize"):
values = [model.get(field) for model in models]
if all(type(value) is int and value > 0 for value in values):
result[field] = min(values)
else:
result.pop(field, None)
rates = [_rate(model.get("credits")) for model in models]
if None in rates:
result.pop("credits", None)
else:
result["credits"] = models[max(range(len(rates)), key=rates.__getitem__)]["credits"]
reasons = [model.get("reasoning") if isinstance(model.get("reasoning"), dict) else {} for model in models]
reasoning = {}
disable = _flag([item.get("canDisableThinking") for item in reasons])
if disable is not None:
reasoning["canDisableThinking"] = disable
efforts = [item.get("supportedEfforts") for item in reasons]
if all(isinstance(values, list) and all(isinstance(value, str) for value in values) for values in efforts):
reasoning["supportedEfforts"] = list(dict.fromkeys(value for value in efforts[0]
if all(value in values for values in efforts)))
for field in ("defaultEffort", "effort", "summary"):
values = [item.get(field) for item in reasons]
if values[0] is not None and all(value == values[0] for value in values):
reasoning[field] = values[0]
if reasoning:
result["reasoning"] = reasoning
else:
result.pop("reasoning", None)
for field in ("isDefault", "relatedModels", "contextWindow", "temperature", "top_p", "top_k", "repetition_penalty"):
values = [model.get(field) for model in models]
if field == "isDefault" or any(value != values[0] for value in values):
result.pop(field, None)
return SharedModel(result, sources)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (broader_impact): The merged declaration starts as a deep copy of the first source model and leaves non-conflict-resolved fields unchanged, so conflicting descriptions, vendor metadata, parameter suggestions, or other allowlisted fields are presented as the effective shared declaration from whichever source happens to come first.

Triggers: When multiple international source accounts provide the same inherited model with different non-core metadata.

Suggested fix: Remove or explicitly reconcile every field that can differ, or omit conflicting fields from the effective shared declaration while retaining the originals in source_variants.

Address review: shared entries previously carried the first source's name, vendor, descriptions, tags and iconUrl unchanged. They are now kept only when every source agrees and omitted on conflict; originals stay visible in source_variants. The supportsToolCall sharing gate is unchanged and matches the system-wide routing usability rule.
@maiphucgiang
maiphucgiang merged commit ffb4cdd into main Sep 16, 2026
8 of 9 checks passed
@maiphucgiang
maiphucgiang deleted the feat/model-capabilities-intl-images branch September 16, 2026 22:04
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.

1 participant