Model capability metadata, preflight guard, international shared catalogs and image merge - #32
Conversation
… 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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThis 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 normalizationsequenceDiagram
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
Entity relationship diagram for model declarations and profile variantserDiagram
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
}
State diagram for model capability guard behaviorstateDiagram-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
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
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
| 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): |
There was a problem hiding this comment.
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.
| 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)): |
| 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) |
There was a problem hiding this comment.
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.
Changes
/v1/modelsand/admin/models: capability states, token limits and per-profile declarations withcatalog_sourceand safesource_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.model_capability_guard, default on, configurable via CLI,CODEBUDDY2API_MODEL_CAPABILITY_GUARDand 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.autodefaults remain independent.Verification
deepseek-v4.1-flashappears on intl-cli as a shared inherited entry at rate 0 and answers through the intl-cli account.glm-5.0image request returns 400unsupported_image_inputand ahy3request above its declared output cap returns 400model_output_limit; after the hot toggle off, the identicalhy3request succeeds with credit 0, and the guard is restored. Local rejections are audited without upstream calls.hy3-xrequests 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.Rollback
Disable
model_capability_guardto 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:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: