feat: normalize OmniRoute model metadata and deduplicate alias entries#20
feat: normalize OmniRoute model metadata and deduplicate alias entries#20Alph4d0g wants to merge 28 commits into
Conversation
… resolution - Add env var fallback for API key in config hook (OMNIROUTE_API_KEY) - Generate modelMetadata from enriched models so OpenCode applies capabilities - Add top-level capability fields (temperature, reasoning, attachment, tool_call, modalities) to provider model output - Generate reasoning effort variants (low/medium/high) for reasoning models - Add provider aliases: glmt/glm→zai-coding-plan, kimi-coding/kmc→moonshotai, gh/github→google - Add subscription fallback: zai-coding-plan→zai, kimi-for-coding→moonshotai - Add model aliases for naming mismatches (kimi-k2.6-thinking→kimi-k2-thinking) - Strip reasoning effort variant suffixes (gpt-5.5-xhigh→gpt-5.5) for lookup - Close 19 of 132 models with 4096 default context limits
Preserve user modelMetadata while merging generated capabilities, propagate new capability fields through models.dev and combo paths, and consolidate alias resolution.
Treat missing attachment capability as unknown instead of false when folding combo model capabilities, while still honoring explicit false values.
Avoid falling back to vision support when models.dev or combo metadata explicitly marks attachments unsupported.
- Fix temperature AND-chain bug in calculateLowestCommonCapabilities by tracking hasTemperatureMetadata - Fix reasoning AND-chain bug in calculateLowestCommonCapabilities by tracking hasReasoningMetadata - Respect explicit supportsAttachment=false for vision models in toProviderModel - Add normalized key candidates to getModelLookupCandidates for better alias/variant matching Addresses feedback from Copilot, Gemini Code Assist, and Kilo Code Bot reviews.
- Expand sanitizeForLog to strip all control chars except tab (was only \r\n) - Export sanitizeForLog from omniroute-combos.ts for reuse in plugin.ts - Sanitize model IDs and external values in plugin.ts debug/warn lines: - Available models list (line 146) - provider.api / apiMode mismatch (lines 209, 215) - Unsupported apiMode option (line 231) - Unsupported baseURL protocol (line 254) - Invalid baseURL (line 260) - Invalid metadata field (line 371) - Intercepting request URL (line 581) - Include models.ts null-data fix in same commit Refs: task-10-log-injection
…ariant+alias, and subscription fallback tests - Task 15: Add test verifying single model and combo-with-self produce identical capabilities - Task 17: Add temperature/reasoning combo tests (mixed defined/undefined, false override, all three capabilities, undefined single model) - Task 18: Add variant suffix stripping + alias resolution integration test - Task 19: Add subscription provider fallback enrichment test
- Add getDummyBaseUrl(port) helper function to eliminate magic numbers - Replace all hardcoded http://localhost:20xxx URLs with helper calls - Keeps assertions and external URLs using hardcoded strings for clarity
…anonicalizing metadata keys
Implementation CompleteThis PR implements the comprehensive normalization + deduplication plan discussed in #18. Key Implementation DetailsNormalization Precedence: Example: If API returns both Deduplication Logic:
Metadata Handling:
Verificationnpm test
# 42 tests passing (38 existing + 4 new)
npm run build
# TypeScript compilation: clean
npx tsc --noEmit
# Type checking: no errorsFiles Changed
Ready for review! |
There was a problem hiding this comment.
Code Review
This pull request introduces a wide range of improvements to the OmniRoute plugin, including a shift to asynchronous logging, enhanced model normalization, and a deduplication system for provider aliases. It expands the model metadata schema to support temperature, reasoning, and attachments, and implements a mechanism for handling model variants like reasoning effort. Review feedback points out critical issues in the implementation of metadata merging and deduplication: specifically, the need for proper precedence and validation in array-based metadata configurations, and the risk of data loss or unreachable logic when merging canonical model entries with their aliases.
| if (Array.isArray(userConfig)) { | ||
| const generatedBlocks = Object.entries(generated).map(([id, metadata]) => ({ | ||
| match: id, | ||
| ...metadata, | ||
| })); | ||
|
|
||
| return [...generatedBlocks, ...userConfig]; | ||
| } |
There was a problem hiding this comment.
There are two issues here:
- Precedence: In most OpenCode matching systems, the first matching block in the
modelMetadataarray wins. By prependinggeneratedBlocks, user-defined overrides inuserConfigwill be ignored for any model already present in the generated metadata.userConfigshould be placed first. - Validation: Unlike the object-based merge logic below, the array-based merge does not validate the user-provided metadata blocks. Invalid metadata should be filtered out to prevent issues in the OpenCode framework.
if (Array.isArray(userConfig)) {
const validUserConfig = userConfig.filter((block) => {
const validation = isValidModelMetadata(block);
if (!validation.valid) {
warn(`Invalid metadata block for match "${sanitizeForLog(String(block.match))}" (field: ${sanitizeForLog(validation.field ?? '')}), skipping`);
return false;
}
return true;
});
const generatedBlocks = Object.entries(generated).map(([id, metadata]) => ({
match: id,
...metadata,
}));
// User config should come first to take precedence in first-match-wins systems
return [...validUserConfig, ...generatedBlocks];
}| if (!canonicalPrefix) { | ||
| seen.set(model.id, model); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
When a canonical model entry is encountered, it currently overwrites any existing entry in the seen map. If an alias entry for the same model was processed first, any metadata unique to that alias entry will be lost. It is better to merge the metadata while ensuring the canonical entry's fields take precedence.
| if (!canonicalPrefix) { | |
| seen.set(model.id, model); | |
| continue; | |
| } | |
| if (!canonicalPrefix) { | |
| const existing = seen.get(model.id); | |
| seen.set(model.id, existing ? { ...existing, ...model } : model); | |
| continue; | |
| } |
| const existing = seen.get(canonicalId); | ||
| if (!existing) { | ||
| // First time seeing this model - store with canonical ID | ||
| seen.set(canonicalId, { | ||
| ...model, | ||
| id: canonicalId, | ||
| }); | ||
| } else { | ||
| // Already have canonical version - merge metadata, prefer non-alias | ||
| const isAlias = providerPrefix !== canonicalPrefix; | ||
| if (!isAlias) { | ||
| // This is the canonical version, overwrite alias | ||
| seen.set(canonicalId, { | ||
| ...model, | ||
| id: canonicalId, | ||
| }); | ||
| } | ||
| // If alias and we already have canonical, drop it | ||
| } |
There was a problem hiding this comment.
The current deduplication logic for aliases has two issues: it contains dead code (the if (!isAlias) block is unreachable because canonicalPrefix is only defined for aliases) and it fails to merge metadata from the alias into an existing canonical entry. Merging metadata ensures that fields present in the alias but missing in the canonical entry are preserved.
const existing = seen.get(canonicalId);
if (!existing) {
seen.set(canonicalId, { ...model, id: canonicalId });
} else {
// Current model is an alias, merge into existing preferring existing fields
// (which might be the canonical entry or a previously seen alias)
seen.set(canonicalId, { ...model, ...existing, id: canonicalId });
}- Fix array-based metadata merge precedence: userConfig now comes first in first-match-wins systems instead of being overridden by generated blocks - Add validation for array-based user metadata blocks to filter out invalid entries before merging - Merge metadata for unknown provider prefixes instead of overwriting, preserving all available metadata - Simplify deduplication logic: remove unreachable isAlias check and merge alias metadata into canonical entries, preferring existing (canonical) fields - Update test expectations to match new array ordering (user first, generated second)
* feat: enrich model capabilities via models.dev and fix provider alias resolution - Add env var fallback for API key in config hook (OMNIROUTE_API_KEY) - Generate modelMetadata from enriched models so OpenCode applies capabilities - Add top-level capability fields (temperature, reasoning, attachment, tool_call, modalities) to provider model output - Generate reasoning effort variants (low/medium/high) for reasoning models - Add provider aliases: glmt/glm→zai-coding-plan, kimi-coding/kmc→moonshotai, gh/github→google - Add subscription fallback: zai-coding-plan→zai, kimi-for-coding→moonshotai - Add model aliases for naming mismatches (kimi-k2.6-thinking→kimi-k2-thinking) - Strip reasoning effort variant suffixes (gpt-5.5-xhigh→gpt-5.5) for lookup - Close 19 of 132 models with 4096 default context limits * fix: address model metadata review feedback Preserve user modelMetadata while merging generated capabilities, propagate new capability fields through models.dev and combo paths, and consolidate alias resolution. * fix: preserve combo attachment metadata Treat missing attachment capability as unknown instead of false when folding combo model capabilities, while still honoring explicit false values. * fix: respect explicit attachment false Avoid falling back to vision support when models.dev or combo metadata explicitly marks attachments unsupported. * fix: address code review issues from PR #18 - Fix temperature AND-chain bug in calculateLowestCommonCapabilities by tracking hasTemperatureMetadata - Fix reasoning AND-chain bug in calculateLowestCommonCapabilities by tracking hasReasoningMetadata - Respect explicit supportsAttachment=false for vision models in toProviderModel - Add normalized key candidates to getModelLookupCandidates for better alias/variant matching Addresses feedback from Copilot, Gemini Code Assist, and Kilo Code Bot reviews. * fix: use nullish coalescing for API key fallback * fix: add runtime validation for modelMetadata merge * fix: improve modelMetadata validation coverage * fix: include modelsDev config in cache key * refactor: extract splitModelId to eliminate DRY violation * docs: document supportsTools default rationale * fix: avoid logging sensitive response data in models fetch error * style: add trailing newline to models-dev.ts * fix: sanitize model IDs in log messages to prevent injection * perf: use async file I/O for logger to prevent event loop blocking * fix: strengthen log sanitization and cover missed interpolation sites - Expand sanitizeForLog to strip all control chars except tab (was only \r\n) - Export sanitizeForLog from omniroute-combos.ts for reuse in plugin.ts - Sanitize model IDs and external values in plugin.ts debug/warn lines: - Available models list (line 146) - provider.api / apiMode mismatch (lines 209, 215) - Unsupported apiMode option (line 231) - Unsupported baseURL protocol (line 254) - Invalid baseURL (line 260) - Invalid metadata field (line 371) - Intercepting request URL (line 581) - Include models.ts null-data fix in same commit Refs: task-10-log-injection * perf: avoid duplicate lookup candidates when alias resolves to same key * types: tighten variants type to prevent invalid reasoning effort values * refactor: extract magic constant 4096 to named defaults * style: remove redundant as const assertions * test(tasks 15,17,18,19): Add model metadata, temperature/reasoning, variant+alias, and subscription fallback tests - Task 15: Add test verifying single model and combo-with-self produce identical capabilities - Task 17: Add temperature/reasoning combo tests (mixed defined/undefined, false override, all three capabilities, undefined single model) - Task 18: Add variant suffix stripping + alias resolution integration test - Task 19: Add subscription provider fallback enrichment test * test(task 20): Replace hardcoded ports with getDummyBaseUrl() helper - Add getDummyBaseUrl(port) helper function to eliminate magic numbers - Replace all hardcoded http://localhost:20xxx URLs with helper calls - Keeps assertions and external URLs using hardcoded strings for clarity * fix: align provider model fields with OpenCode plugin types * types: add OmniRoute native fields (snake_case, capabilities) to OmniRouteModel * feat: normalize all OmniRoute field variants (snake_case, capabilities) * feat: add provider alias-to-canonical mapping for deduplication * test: verify normalization of snake_case and capabilities fields * fix: preserve user metadata by only deduplicating known aliases and canonicalizing metadata keys * fix: address gemini-code-assist review feedback on PR #20 - Fix array-based metadata merge precedence: userConfig now comes first in first-match-wins systems instead of being overridden by generated blocks - Add validation for array-based user metadata blocks to filter out invalid entries before merging - Merge metadata for unknown provider prefixes instead of overwriting, preserving all available metadata - Simplify deduplication logic: remove unreachable isAlias check and merge alias metadata into canonical entries, preferring existing (canonical) fields - Update test expectations to match new array ordering (user first, generated second) * chore: bump version to 1.2.0, update CHANGELOG and README - Bump version from 1.1.4 to 1.2.0 in package.json - Add comprehensive CHANGELOG entry for v1.2.0 covering: - Model metadata normalization (camelCase, snake_case, capabilities) - Provider alias deduplication system - Array-based metadata validation and precedence fixes - Metadata key canonicalization - All gemini-code-assist review feedback addressed - Update README with: - New features: metadata normalization, alias deduplication, variant support - Updated OmniRouteModel type documentation with native fields - Star History graph at the bottom --------- Co-authored-by: Alexander Bien <abien@gmx.net>
Summary
This PR addresses the review feedback from #18 by implementing comprehensive model metadata normalization and deduplication:
Changes
Extended Types (
src/types.ts)context_length,max_input_tokens,max_output_tokens) toOmniRouteModelcapabilitiesobject withvision,tool_calling,reasoning,thinking,attachment,temperaturefieldsvision,tool_calling) for direct API mappingComprehensive Normalization (
src/models.ts)normalizeModel()functionProvider Alias Mapping (
src/constants.ts)PROVIDER_ALIAS_TO_CANONICALmapping for known aliases:ollamacloud→ollama-cloudcc→claudegh→githubcx→codexkr→kiroif→qoderSmart Deduplication (
src/models.ts)deduplicateModels()that groups by canonical provider+model keyMetadata Key Canonicalization (
src/plugin.ts)resolveProviderAliasForMetadata()to canonicalize model IDs for metadata lookupsisProviderAlias()helpercx/gpt-5.5) is merged into canonical keys (e.g.,codex/gpt-5.5)Test Coverage (
test/models.test.mjs)normalizeModel reads snake_case fields- verifies capabilities object and snake_case field readingnormalizeModel prefers camelCase over snake_case- verifies precedence rulesdeduplication removes alias when canonical exists- verifies canonical preferencededuplication keeps alias when canonical is missing- verifies alias normalizationTest Results
All 42 tests passing:
Design Decisions
Backward Compatibility