Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2b391e9
feat: enrich model capabilities via models.dev and fix provider alias…
abien May 15, 2026
254201f
fix: address model metadata review feedback
abien May 15, 2026
0103477
fix: preserve combo attachment metadata
abien May 15, 2026
5df51f9
fix: respect explicit attachment false
abien May 15, 2026
9ca663b
fix: address code review issues from PR #18
Alph4d0g May 16, 2026
c8fa121
fix: use nullish coalescing for API key fallback
Alph4d0g May 16, 2026
c20ec3b
fix: add runtime validation for modelMetadata merge
Alph4d0g May 16, 2026
44944ff
fix: improve modelMetadata validation coverage
Alph4d0g May 16, 2026
b694043
fix: include modelsDev config in cache key
Alph4d0g May 16, 2026
95cdf39
refactor: extract splitModelId to eliminate DRY violation
Alph4d0g May 16, 2026
1b2718a
docs: document supportsTools default rationale
Alph4d0g May 16, 2026
78dae51
fix: avoid logging sensitive response data in models fetch error
Alph4d0g May 16, 2026
30fb321
style: add trailing newline to models-dev.ts
Alph4d0g May 16, 2026
0b1519c
fix: sanitize model IDs in log messages to prevent injection
Alph4d0g May 16, 2026
02d79b8
perf: use async file I/O for logger to prevent event loop blocking
Alph4d0g May 16, 2026
0b049a6
fix: strengthen log sanitization and cover missed interpolation sites
Alph4d0g May 16, 2026
f08a553
perf: avoid duplicate lookup candidates when alias resolves to same key
Alph4d0g May 16, 2026
2816f4c
types: tighten variants type to prevent invalid reasoning effort values
Alph4d0g May 16, 2026
be56ca4
refactor: extract magic constant 4096 to named defaults
Alph4d0g May 16, 2026
b3226b2
style: remove redundant as const assertions
Alph4d0g May 16, 2026
c92d7a1
test(tasks 15,17,18,19): Add model metadata, temperature/reasoning, v…
Alph4d0g May 16, 2026
114969f
test(task 20): Replace hardcoded ports with getDummyBaseUrl() helper
Alph4d0g May 16, 2026
3255e63
fix: align provider model fields with OpenCode plugin types
Alph4d0g May 16, 2026
8ba5ed8
types: add OmniRoute native fields (snake_case, capabilities) to Omni…
Alph4d0g May 17, 2026
36ccb28
feat: normalize all OmniRoute field variants (snake_case, capabilities)
Alph4d0g May 17, 2026
a22d539
feat: add provider alias-to-canonical mapping for deduplication
Alph4d0g May 17, 2026
1f043a2
test: verify normalization of snake_case and capabilities fields
Alph4d0g May 17, 2026
76b727f
fix: preserve user metadata by only deduplicating known aliases and c…
Alph4d0g May 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
950 changes: 950 additions & 0 deletions docs/superpowers/plans/2026-05-16-pr18-review-fixes.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,27 @@ export const MODEL_CACHE_TTL = 5 * 60 * 1000;
*/
export const REQUEST_TIMEOUT = 30000;

/**
* Default model limits
*/
export const DEFAULT_CONTEXT_LIMIT = 4096;
export const DEFAULT_OUTPUT_LIMIT = 4096;

/**
* models.dev enrichment defaults
*/
export const MODELS_DEV_DEFAULT_URL = 'https://models.dev/api.json';
export const MODELS_DEV_CACHE_TTL = 24 * 60 * 60 * 1000;
export const MODELS_DEV_TIMEOUT_MS = 1000;

/**
* Provider alias-to-canonical mapping for deduplication
*/
export const PROVIDER_ALIAS_TO_CANONICAL: Record<string, string> = {
ollamacloud: 'ollama-cloud',
cc: 'claude',
gh: 'github',
cx: 'codex',
kr: 'kiro',
if: 'qoder',
};
63 changes: 18 additions & 45 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { appendFileSync, readdirSync, statSync, existsSync } from 'fs';
import { readdirSync, statSync, existsSync } from 'fs';
import { appendFile } from 'fs/promises';
import { join } from 'path';
import { homedir } from 'os';

Expand Down Expand Up @@ -32,62 +33,34 @@ function findCurrentLogFile(): string | null {
let cachedLogFile: string | null = findCurrentLogFile();

function getLogFile(): string | null {
if (cachedLogFile === null) {
// Re-scan if no file found at module load (OpenCode may create one later)
if (cachedLogFile === null || !existsSync(cachedLogFile)) {
// Re-scan if no file found at module load or if cached file was deleted (log rotation)
cachedLogFile = findCurrentLogFile();
}
return cachedLogFile;
}

function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
typeof (error as NodeJS.ErrnoException).code === 'string'
);
}

function writeLog(level: string, message: string): void {
let logFile = getLogFile();
if (!logFile) return;

// Check if cached file still exists (handles log rotation)
if (!existsSync(logFile)) {
cachedLogFile = findCurrentLogFile();
logFile = cachedLogFile;
if (!logFile) return;
}

function formatLogLine(level: string, message: string): string {
const timestamp = new Date().toISOString();
const line = `${level.padEnd(5)} ${timestamp} +0ms service=omniroute ${message}\n`;

try {
appendFileSync(logFile, line);
} catch (error: unknown) {
if (isNodeError(error) && error.code === 'ENOENT') {
// Log file was deleted, re-scan
cachedLogFile = findCurrentLogFile();
// Retry once with new file
const newLogFile = cachedLogFile;
if (newLogFile) {
try {
appendFileSync(newLogFile, line);
} catch {
// Silently fail on second attempt
}
}
}
// Silently fail for all other errors
}
return `${level.padEnd(5)} ${timestamp} +0ms service=omniroute ${message}\n`;
}

export function warn(message: string): void {
writeLog('WARN', message);
const logFile = getLogFile();
if (!logFile) return;

const line = formatLogLine('WARN', message);
// Fire-and-forget: don't await, don't crash on error
appendFile(logFile, line).catch(() => {});
}

export function debug(message: string): void {
// Strict comparison: only "1" enables debug logging
if (process.env.OMNIROUTE_DEBUG !== '1') return;
writeLog('DEBUG', message);

const logFile = getLogFile();
if (!logFile) return;

const line = formatLogLine('DEBUG', message);
appendFile(logFile, line).catch(() => {});
}
111 changes: 108 additions & 3 deletions src/models-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ export function modelsDevToMetadata(model: ModelsDevModel): OmniRouteModelMetada
metadata.maxTokens = model.limit.output;
}

if (model.temperature !== undefined) {
metadata.supportsTemperature = model.temperature;
}

if (model.reasoning !== undefined) {
metadata.supportsReasoning = model.reasoning;
}

if (model.attachment !== undefined) {
metadata.supportsAttachment = model.attachment;
}

// Derive vision support from modalities
if (model.modalities?.input?.includes('image')) {
metadata.supportsVision = true;
Expand All @@ -256,8 +268,6 @@ export function modelsDevToMetadata(model: ModelsDevModel): OmniRouteModelMetada
metadata.supportsTools = true;
}



// Pricing
if (model.cost?.input !== undefined || model.cost?.output !== undefined) {
metadata.pricing = {};
Expand Down Expand Up @@ -292,6 +302,12 @@ export function calculateLowestCommonCapabilities(
let minMaxTokens: number | undefined;
let allSupportVision = true;
let allSupportTools = true;
let allSupportTemperature = true;
let hasTemperatureMetadata = false;
let allSupportReasoning = true;
let hasReasoningMetadata = false;
let allSupportAttachment = true;
let hasAttachmentMetadata = false;
let allSupportStreaming = true;

for (const model of models) {
Expand All @@ -312,8 +328,26 @@ export function calculateLowestCommonCapabilities(
allSupportVision = allSupportVision && supportsVision;

// Tools: all must support it
// For combos: only advertise tools if ALL underlying models explicitly support them
// This is intentionally stricter than single-model defaults because a combo
// with one tool-less model cannot reliably use tools across all backends
const supportsTools = model.tool_call === true;
allSupportTools = allSupportTools && supportsTools;

if (model.temperature !== undefined) {
hasTemperatureMetadata = true;
allSupportTemperature = allSupportTemperature && model.temperature;
}

if (model.reasoning !== undefined) {
hasReasoningMetadata = true;
allSupportReasoning = allSupportReasoning && model.reasoning;
}

if (model.attachment !== undefined) {
hasAttachmentMetadata = true;
allSupportAttachment = allSupportAttachment && model.attachment;
}
}

const result: OmniRouteModelMetadata = {};
Expand All @@ -334,6 +368,24 @@ export function calculateLowestCommonCapabilities(
result.supportsTools = true;
}

if (hasTemperatureMetadata && allSupportTemperature) {
result.supportsTemperature = true;
} else if (hasTemperatureMetadata) {
result.supportsTemperature = false;
}

if (hasReasoningMetadata && allSupportReasoning) {
result.supportsReasoning = true;
} else if (hasReasoningMetadata) {
result.supportsReasoning = false;
}

if (hasAttachmentMetadata && allSupportAttachment) {
result.supportsAttachment = true;
} else if (hasAttachmentMetadata) {
result.supportsAttachment = false;
}

// Streaming is generally supported by all modern models
if (allSupportStreaming) {
result.supportsStreaming = true;
Expand All @@ -343,6 +395,31 @@ export function calculateLowestCommonCapabilities(
}


/**
* Subscription → public provider fallback map.
* When a subscription provider (e.g. zai-coding-plan) lacks a model,
* try its public counterpart (e.g. zai) before giving up.
*/
export const SUBSCRIPTION_FALLBACKS: Record<string, string> = {
'zai-coding-plan': 'zai',
'kimi-for-coding': 'moonshotai',
'github-models': 'google',
};

/**
* Known model ID mismatches between OmniRoute and models.dev.
* Maps OmniRoute model names to their models.dev equivalents.
*/
export const MODEL_ALIASES: Record<string, string> = {
'kimi-k2.6-thinking': 'kimi-k2-thinking',
'kimi-k2.6-thinking-turbo': 'kimi-k2-thinking-turbo',
};

export function resolveModelAlias(modelKey: string): string {
const lower = modelKey.toLowerCase();
return MODEL_ALIASES[lower] ?? MODEL_ALIASES[normalizeModelKey(lower)] ?? modelKey;
}

/**
* Resolve provider alias using config and defaults
*/
Expand Down Expand Up @@ -372,8 +449,36 @@ export function resolveProviderAlias(
openrouter: 'openrouter',
perplexity: 'perplexity',
cohere: 'cohere',
glmt: 'zai-coding-plan',
glm: 'zai-coding-plan',
'kimi-coding': 'moonshotai',
kmc: 'moonshotai',
gh: 'google',
github: 'google',
...config?.modelsDev?.providerAliases,
};

return aliases[lower] ?? lower;
}
}

/**
* Get the public fallback provider for a subscription provider.
* Returns null if no fallback exists.
*/
export function getSubscriptionFallback(provider: string): string | null {
return SUBSCRIPTION_FALLBACKS[provider.toLowerCase()] ?? null;
}

/**
* Strip reasoning effort variant suffix from a model name.
* Returns the base model name and true if a suffix was stripped.
*/
export function stripVariantSuffix(modelKey: string): { base: string; stripped: boolean } {
const variantPattern = /-(low|medium|high|xhigh)$/i;
const match = modelKey.match(variantPattern);
if (match) {
return { base: modelKey.slice(0, match.index), stripped: true };
}
return { base: modelKey, stripped: false };
}

Loading
Loading