Skip to content

Commit 74fff3e

Browse files
committed
feat: add PolicyAwareRouter, UncensoredModelCatalog, PolicyAwareImageRouter
1 parent d28a04c commit 74fff3e

File tree

8 files changed

+1066
-0
lines changed

8 files changed

+1066
-0
lines changed

src/core/llm/routing/IModelRouter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ export interface ModelRouteParams {
7070
userApiKeys?: Record<string, string>; // ProviderId -> ApiKey
7171
/** Additional custom parameters or context to aid routing decisions. */
7272
customContext?: Record<string, any>;
73+
74+
/**
75+
* Content policy tier governing this request. Drives model selection:
76+
* safe/standard use default censored models, mature/private-adult route
77+
* to uncensored models via PolicyAwareRouter.
78+
*/
79+
policyTier?: 'safe' | 'standard' | 'mature' | 'private-adult';
80+
81+
/**
82+
* Finer-grained content intent hint. A mature session doing combat narration
83+
* vs. romance has different model preferences.
84+
*/
85+
contentIntent?: 'general' | 'romantic' | 'erotic' | 'violent' | 'horror';
7386
}
7487

7588
/**
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* @fileoverview Policy-aware model router that selects uncensored LLMs for
3+
* mature/private-adult content policy tiers.
4+
*
5+
* For safe/standard tiers (or when no policy tier is specified), the router
6+
* delegates to an optional base router or returns null, letting the caller
7+
* fall back to its own default model. For mature/private-adult tiers, it
8+
* consults the {@link UncensoredModelCatalog} and returns an OpenRouter model
9+
* wrapped in a minimal {@link ModelRouteResult}.
10+
*
11+
* @module core/llm/routing/PolicyAwareRouter
12+
*/
13+
14+
import type { IModelRouter, ModelRouteParams, ModelRouteResult } from './IModelRouter';
15+
import type { ModelInfo } from '../providers/IProvider';
16+
import type {
17+
UncensoredModelCatalog,
18+
PolicyTier,
19+
CatalogEntry,
20+
} from './UncensoredModelCatalog';
21+
22+
/**
23+
* Manual override map: policyTier -> fixed modelId.
24+
* When provided, bypasses catalog lookup for the specified tier.
25+
*/
26+
export type PolicyOverrides = Partial<Record<PolicyTier, string>>;
27+
28+
/**
29+
* Policy-aware router that wraps an optional base router and injects
30+
* uncensored model selection for mature/private-adult policy tiers.
31+
*/
32+
export class PolicyAwareRouter implements IModelRouter {
33+
public readonly routerId = 'policy_aware_router_v1';
34+
35+
private readonly catalog: UncensoredModelCatalog;
36+
private readonly baseRouter: IModelRouter | null;
37+
private readonly overrides: PolicyOverrides;
38+
private readonly defaultPolicyTier: PolicyTier | undefined;
39+
40+
/**
41+
* @param catalog - Uncensored model catalog for mature/private-adult lookup.
42+
* @param baseRouter - Optional delegate for safe/standard tiers.
43+
* @param overrides - Per-tier model ID overrides that bypass catalog lookup.
44+
* @param defaultPolicyTier - Fallback tier when params.policyTier is absent.
45+
*/
46+
constructor(
47+
catalog: UncensoredModelCatalog,
48+
baseRouter?: IModelRouter | null,
49+
overrides?: PolicyOverrides,
50+
defaultPolicyTier?: PolicyTier,
51+
) {
52+
this.catalog = catalog;
53+
this.baseRouter = baseRouter ?? null;
54+
this.overrides = overrides ?? {};
55+
this.defaultPolicyTier = defaultPolicyTier;
56+
}
57+
58+
/**
59+
* No-op initialization. The PolicyAwareRouter is stateless beyond its
60+
* constructor arguments; it does not require async setup.
61+
*/
62+
async initialize(
63+
_config: Record<string, any>,
64+
_providerManager: any,
65+
_promptEngine?: any,
66+
): Promise<void> {
67+
// Intentionally empty: catalog is injected, no async work needed.
68+
}
69+
70+
/**
71+
* Select a model based on the request's policy tier.
72+
*
73+
* - safe / standard / absent (no default): delegate to baseRouter or return null.
74+
* - mature / private-adult: check overrides, then catalog, return OpenRouter result.
75+
*/
76+
async selectModel(
77+
params: ModelRouteParams,
78+
availableModels?: ModelInfo[],
79+
): Promise<ModelRouteResult | null> {
80+
const tier: PolicyTier | undefined =
81+
params.policyTier ?? this.defaultPolicyTier;
82+
83+
// Safe / standard / absent tier: delegate or return null
84+
if (!tier || tier === 'safe' || tier === 'standard') {
85+
if (this.baseRouter) {
86+
return this.baseRouter.selectModel(params, availableModels);
87+
}
88+
return null;
89+
}
90+
91+
// Check per-tier override first
92+
const overrideModelId = this.overrides[tier];
93+
if (overrideModelId) {
94+
return this.buildResult(overrideModelId, 'openrouter', `Override for ${tier} tier`);
95+
}
96+
97+
// Consult catalog
98+
const entry = this.catalog.getPreferredTextModel(tier, params.contentIntent);
99+
if (entry) {
100+
return this.buildResult(
101+
entry.modelId,
102+
entry.providerId,
103+
`Catalog selection for ${tier} tier (${entry.displayName})`,
104+
);
105+
}
106+
107+
return null;
108+
}
109+
110+
/**
111+
* Build a minimal {@link ModelRouteResult} with a stub provider.
112+
* The provider stub satisfies the interface contract while signalling to
113+
* upstream consumers that the actual provider instance must be resolved
114+
* from the provider manager using the returned providerId.
115+
*/
116+
private buildResult(
117+
modelId: string,
118+
providerId: string,
119+
reasoning: string,
120+
): ModelRouteResult {
121+
return {
122+
provider: {
123+
providerId,
124+
isInitialized: false,
125+
async initialize() {},
126+
async generateCompletion() {
127+
throw new Error('Stub provider — resolve via AIModelProviderManager');
128+
},
129+
async *generateCompletionStream() {
130+
throw new Error('Stub provider — resolve via AIModelProviderManager');
131+
},
132+
async generateEmbeddings() {
133+
throw new Error('Stub provider — resolve via AIModelProviderManager');
134+
},
135+
async listAvailableModels() {
136+
return [];
137+
},
138+
async getModelInfo() {
139+
return undefined;
140+
},
141+
async checkHealth() {
142+
return { isHealthy: false };
143+
},
144+
async shutdown() {},
145+
},
146+
modelId,
147+
modelInfo: {
148+
modelId,
149+
providerId,
150+
capabilities: ['chat'],
151+
},
152+
reasoning,
153+
confidence: 0.85,
154+
metadata: { source: this.routerId, policyRouted: true },
155+
};
156+
}
157+
}

0 commit comments

Comments
 (0)