Skip to content

Commit 351f150

Browse files
committed
fix: apply model metadata overrides to provider models
1 parent 4362383 commit 351f150

2 files changed

Lines changed: 802 additions & 15 deletions

File tree

‎src/plugin.ts‎

Lines changed: 253 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
DEFAULT_CONTEXT_LIMIT,
2020
DEFAULT_OUTPUT_LIMIT,
2121
} from './constants.js';
22-
import { fetchModels, resolveProviderAliasForMetadata, isProviderAlias } from './models.js';
22+
import { fetchModels, resolveProviderAliasForMetadata } from './models.js';
2323
import { warn, debug } from './logger.js';
2424
import { sanitizeForLog } from './omniroute-combos.js';
2525

@@ -31,6 +31,14 @@ type AuthHook = NonNullable<Hooks['auth']>;
3131
type AuthLoader = NonNullable<AuthHook['loader']>;
3232
type AuthAccessor = Parameters<AuthLoader>[0];
3333
type ProviderDefinition = Parameters<AuthLoader>[1];
34+
const RAW_MODEL_METADATA = Symbol('omniroute.rawModelMetadata');
35+
const RAW_MODEL_METADATA_OPTION = '__omnirouteRawModelMetadata';
36+
const MODELS_GENERATED_BY_PLUGIN = Symbol('omniroute.modelsGeneratedByPlugin');
37+
const MODELS_GENERATED_BY_PLUGIN_OPTION = '__omnirouteModelsGeneratedByPlugin';
38+
type OptionsWithRawModelMetadata = Record<string, unknown> & {
39+
[RAW_MODEL_METADATA]?: unknown;
40+
[MODELS_GENERATED_BY_PLUGIN]?: unknown;
41+
};
3442

3543
export const OmniRouteAuthPlugin: Plugin = async (_input) => {
3644
return {
@@ -40,6 +48,7 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
4048
const baseUrl = getBaseUrl(existingProvider?.options);
4149
const apiMode = getApiMode(existingProvider?.options);
4250
const providerApi = resolveProviderApi(existingProvider?.api, apiMode);
51+
const rawUserModelMetadata = getRawUserModelMetadata(existingProvider?.options);
4352

4453
// Eagerly fetch models for OpenCode <=1.14.48 (which read models from config hook).
4554
// OpenCode >=1.14.49 uses the provider hook below instead.
@@ -55,6 +64,11 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
5564
warn(`Eager model fetch failed, using defaults: ${error}`);
5665
}
5766

67+
const effectiveModels = applyModelMetadataOverrides(
68+
models,
69+
rawUserModelMetadata,
70+
);
71+
5872
const generatedModelMetadata: Record<string, OmniRouteModelMetadata> = {};
5973
for (const model of models) {
6074
// Use canonical ID for metadata keys to match user config
@@ -73,26 +87,37 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
7387
}
7488

7589
const modelMetadata = mergeModelMetadata(
76-
existingProvider?.options?.modelMetadata,
90+
rawUserModelMetadata,
7791
generatedModelMetadata,
7892
);
7993

94+
const providerOptions: Record<string, unknown> = {
95+
...(existingProvider?.options ?? {}),
96+
baseURL: baseUrl,
97+
apiMode,
98+
modelMetadata,
99+
};
100+
setRawUserModelMetadata(providerOptions, rawUserModelMetadata);
101+
102+
const hasExistingModels = Boolean(
103+
existingProvider?.models && Object.keys(existingProvider.models).length > 0,
104+
);
105+
const shouldPreserveExistingModels = hasExistingModels && !getModelsGeneratedByPlugin(
106+
existingProvider?.options,
107+
);
108+
const providerModels = shouldPreserveExistingModels
109+
? existingProvider?.models
110+
: toProviderModels(effectiveModels, baseUrl);
111+
setModelsGeneratedByPlugin(providerOptions, !shouldPreserveExistingModels);
112+
80113
providers[OMNIROUTE_PROVIDER_ID] = {
81114
...existingProvider,
82115
name: existingProvider?.name ?? OMNIROUTE_PROVIDER_NAME,
83116
api: providerApi,
84117
npm: existingProvider?.npm ?? OMNIROUTE_PROVIDER_NPM,
85118
env: existingProvider?.env ?? OMNIROUTE_PROVIDER_ENV,
86-
options: {
87-
...(existingProvider?.options ?? {}),
88-
baseURL: baseUrl,
89-
apiMode,
90-
modelMetadata,
91-
},
92-
models:
93-
existingProvider?.models && Object.keys(existingProvider.models).length > 0
94-
? existingProvider.models
95-
: toProviderModels(models, baseUrl),
119+
options: providerOptions,
120+
models: providerModels,
96121
};
97122

98123
config.provider = providers;
@@ -107,12 +132,20 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
107132
if (ctx.auth?.type === 'api' && ctx.auth.key) {
108133
const runtimeConfig = createRuntimeConfig(provider.options, ctx.auth.key);
109134
const models = await fetchModels(runtimeConfig, ctx.auth.key, false);
110-
return toProviderModels(models, baseUrl);
135+
const effectiveModels = applyModelMetadataOverrides(
136+
models,
137+
getRawUserModelMetadata(provider.options),
138+
);
139+
return toProviderModels(effectiveModels, baseUrl);
111140
}
112141

113142
// No auth yet (user hasn't /connect'd): return built-in defaults.
114143
// This ensures models have the correct metadata (like api.url) to work with the plugin.
115-
return toProviderModels(OMNIROUTE_DEFAULT_MODELS, baseUrl);
144+
const effectiveModels = applyModelMetadataOverrides(
145+
OMNIROUTE_DEFAULT_MODELS,
146+
getRawUserModelMetadata(provider.options),
147+
);
148+
return toProviderModels(effectiveModels, baseUrl);
116149
},
117150
},
118151
auth: createAuthHook(),
@@ -155,7 +188,11 @@ async function loadProviderOptions(
155188
models = OMNIROUTE_DEFAULT_MODELS;
156189
}
157190

158-
replaceProviderModels(provider, toProviderModels(models, config.baseUrl));
191+
const effectiveModels = applyModelMetadataOverrides(
192+
models,
193+
getRawUserModelMetadata(provider.options),
194+
);
195+
replaceProviderModels(provider, toProviderModels(effectiveModels, config.baseUrl));
159196
if (isRecord(provider.models)) {
160197
debug(`Provider models hydrated: ${Object.keys(provider.models).length}`);
161198
}
@@ -340,6 +377,72 @@ function getModelMetadataConfig(
340377
return undefined;
341378
}
342379

380+
function getRawUserModelMetadata(options: Record<string, unknown> | undefined): unknown {
381+
if (!options) return undefined;
382+
const optionsWithRaw = options as OptionsWithRawModelMetadata;
383+
// Preserve raw user-authored modelMetadata separately from generated compatibility
384+
// metadata. The non-enumerable Symbol is the in-memory fast path; if OpenCode
385+
// clones/serializes options between lifecycle hooks, the internal option field
386+
// survives and distinguishes "no raw metadata" (null) from generated metadata.
387+
if (RAW_MODEL_METADATA in optionsWithRaw) {
388+
return optionsWithRaw[RAW_MODEL_METADATA];
389+
}
390+
if (RAW_MODEL_METADATA_OPTION in options) {
391+
return options[RAW_MODEL_METADATA_OPTION] === null
392+
? undefined
393+
: options[RAW_MODEL_METADATA_OPTION];
394+
}
395+
return options.modelMetadata;
396+
}
397+
398+
function setRawUserModelMetadata(options: Record<string, unknown>, rawUserConfig: unknown): void {
399+
options[RAW_MODEL_METADATA_OPTION] = serializeRawModelMetadataForOption(rawUserConfig) ?? null;
400+
Object.defineProperty(options, RAW_MODEL_METADATA, {
401+
value: rawUserConfig,
402+
enumerable: false,
403+
configurable: true,
404+
writable: true,
405+
});
406+
}
407+
408+
function serializeRawModelMetadataForOption(raw: unknown): unknown {
409+
if (!Array.isArray(raw)) return raw;
410+
411+
return raw.map((block) => {
412+
if (!isRecord(block) || !isRegExp(block.match)) return block;
413+
414+
return {
415+
...block,
416+
match: {
417+
source: block.match.source,
418+
flags: block.match.flags,
419+
},
420+
};
421+
});
422+
}
423+
424+
function getModelsGeneratedByPlugin(options: Record<string, unknown> | undefined): boolean {
425+
if (!options) return false;
426+
const optionsWithMarker = options as OptionsWithRawModelMetadata;
427+
if (MODELS_GENERATED_BY_PLUGIN in optionsWithMarker) {
428+
return optionsWithMarker[MODELS_GENERATED_BY_PLUGIN] === true;
429+
}
430+
return options[MODELS_GENERATED_BY_PLUGIN_OPTION] === true;
431+
}
432+
433+
function setModelsGeneratedByPlugin(
434+
options: Record<string, unknown>,
435+
generatedByPlugin: boolean,
436+
): void {
437+
options[MODELS_GENERATED_BY_PLUGIN_OPTION] = generatedByPlugin ? true : null;
438+
Object.defineProperty(options, MODELS_GENERATED_BY_PLUGIN, {
439+
value: generatedByPlugin,
440+
enumerable: false,
441+
configurable: true,
442+
writable: true,
443+
});
444+
}
445+
343446
function getStringRecord(value: unknown): Record<string, string> | undefined {
344447
if (!isRecord(value)) return undefined;
345448

@@ -402,6 +505,141 @@ function mergeModelMetadata(
402505
return generated;
403506
}
404507

508+
function applyModelMetadataOverrides(
509+
models: OmniRouteModel[],
510+
rawUserConfig: unknown,
511+
): OmniRouteModel[] {
512+
const userConfig = getModelMetadataConfig({ modelMetadata: rawUserConfig });
513+
if (!userConfig) return models;
514+
515+
if (Array.isArray(userConfig)) {
516+
const validBlocks = userConfig.filter((block) => {
517+
const validation = isValidModelMetadata(block);
518+
if (!validation.valid) {
519+
warn(`Invalid metadata block for match "${sanitizeForLog(String(block.match))}" (field: ${sanitizeForLog(validation.field ?? '')}), skipping`);
520+
return false;
521+
}
522+
return true;
523+
});
524+
525+
const modelsWithOverrides = models.map((model) => {
526+
const canonicalId = resolveProviderAliasForMetadata(model.id);
527+
const block = validBlocks.find((candidate) =>
528+
metadataBlockMatches(candidate.match, model.id, canonicalId),
529+
);
530+
if (!block) return model;
531+
532+
return {
533+
...model,
534+
...extractModelMetadata(block),
535+
};
536+
});
537+
538+
const existingModels = modelsWithOverrides.map((model) => ({
539+
id: model.id,
540+
canonicalId: resolveProviderAliasForMetadata(model.id),
541+
}));
542+
const missingModels: OmniRouteModel[] = [];
543+
for (const block of validBlocks) {
544+
if (block.addIfMissing !== true || typeof block.match !== 'string') continue;
545+
if (!isConcreteModelIdMatch(block.match)) continue;
546+
547+
const id = resolveProviderAliasForMetadata(block.match);
548+
const alreadyExists = existingModels.some((model) =>
549+
metadataBlockMatches(block.match, model.id, model.canonicalId),
550+
) || missingModels.some((model) => model.id === id);
551+
if (alreadyExists) continue;
552+
553+
missingModels.push({
554+
id,
555+
name: block.name ?? id,
556+
...extractModelMetadata(block),
557+
});
558+
}
559+
560+
return [...modelsWithOverrides, ...missingModels];
561+
}
562+
563+
const overrides: Record<string, OmniRouteModelMetadata> = {};
564+
for (const [id, metadata] of Object.entries(userConfig)) {
565+
const validation = isValidModelMetadata(metadata);
566+
if (!validation.valid) {
567+
warn(`Invalid metadata for model "${sanitizeForLog(id)}" (field: ${sanitizeForLog(validation.field ?? '')}), skipping`);
568+
continue;
569+
}
570+
571+
const canonicalId = resolveProviderAliasForMetadata(id);
572+
overrides[canonicalId] = {
573+
...(overrides[canonicalId] ?? {}),
574+
...extractModelMetadata(metadata),
575+
};
576+
}
577+
578+
return models.map((model) => {
579+
const canonicalId = resolveProviderAliasForMetadata(model.id);
580+
const metadata = overrides[canonicalId];
581+
if (!metadata) return model;
582+
583+
return {
584+
...model,
585+
...metadata,
586+
};
587+
});
588+
}
589+
590+
function metadataBlockMatches(match: unknown, modelId: string, canonicalId: string): boolean {
591+
if (typeof match === 'string') {
592+
const canonicalMatch = resolveProviderAliasForMetadata(match);
593+
return (
594+
match === modelId ||
595+
match === canonicalId ||
596+
canonicalMatch === modelId ||
597+
canonicalMatch === canonicalId ||
598+
metadataMatcherMatches(match, modelId) ||
599+
metadataMatcherMatches(match, canonicalId)
600+
);
601+
}
602+
603+
return metadataMatcherMatches(match, modelId) || metadataMatcherMatches(match, canonicalId);
604+
}
605+
606+
function metadataMatcherMatches(match: unknown, modelId: string): boolean {
607+
if (typeof match === 'string') {
608+
if (match === modelId) return true;
609+
610+
try {
611+
return new RegExp(match).test(modelId);
612+
} catch {
613+
return false;
614+
}
615+
}
616+
617+
const regexp = coerceRegExp(match);
618+
if (!regexp) return false;
619+
regexp.lastIndex = 0;
620+
return regexp.test(modelId);
621+
}
622+
623+
function isConcreteModelIdMatch(match: string): boolean {
624+
return !/[\\^$*+?()[\]{}|]/u.test(match);
625+
}
626+
627+
function extractModelMetadata(value: OmniRouteModelMetadata): OmniRouteModelMetadata {
628+
return {
629+
...('name' in value ? { name: value.name } : {}),
630+
...('description' in value ? { description: value.description } : {}),
631+
...('contextWindow' in value ? { contextWindow: value.contextWindow } : {}),
632+
...('maxTokens' in value ? { maxTokens: value.maxTokens } : {}),
633+
...('supportsStreaming' in value ? { supportsStreaming: value.supportsStreaming } : {}),
634+
...('supportsVision' in value ? { supportsVision: value.supportsVision } : {}),
635+
...('supportsTools' in value ? { supportsTools: value.supportsTools } : {}),
636+
...('supportsTemperature' in value ? { supportsTemperature: value.supportsTemperature } : {}),
637+
...('supportsReasoning' in value ? { supportsReasoning: value.supportsReasoning } : {}),
638+
...('supportsAttachment' in value ? { supportsAttachment: value.supportsAttachment } : {}),
639+
...('pricing' in value ? { pricing: value.pricing } : {}),
640+
};
641+
}
642+
405643
function isRegExp(value: unknown): value is RegExp {
406644
return Object.prototype.toString.call(value) === '[object RegExp]';
407645
}

0 commit comments

Comments
 (0)