Experimental Jev-assisted model and effort selection for Claude Code: observe, pin, off, and opt-in auto.

I've been fascinated by Jev and the idea of small, focused models making decisions inside software. I've been interested in this approach for the last couple of years, and I'm already using these ideas in several parts of my workflow.
One thing I need every day is intelligent model switching. When I work in Claude Code, I don't want to use Fable, Opus or Sonnet for everything. I want the model to fit the work, with the option to take control whenever I need to. That's why I built JevShift, and why I'm sharing it.
JevShift asks Jev to choose a model and thinking effort using your request, recent conversation, plan and tool results. It can change both inside the same Claude Code conversation.
| Work | Preferred model |
|---|---|
| Substantial planning and design | Fable |
| Implementing an agreed plan and debugging | Opus |
| Small edits, explanations and documentation | Sonnet |
Effort is chosen separately: low for routine work, medium for bounded problems, high for substantial reasoning, xhigh for difficult interactions, and max for exceptional analysis. An agreed plan can still need high effort to implement.
These are preferences, not guaranteed choices. New sessions start in observe mode: you see recommendations while Claude keeps its current model and effort. Turn on auto when you're ready, or pin either choice yourself.
Model versions follow Claude Code's sonnet, opus and fable aliases. Keep Claude Code updated; JevShift resolves them at startup. Use /jevshift refresh to check again in an existing session. An exact model-ID pin stays on that version.
Experimental alpha. Tested on macOS with Claude Code 2.1.280. Requires curl 8.4+, your Claude login and model access. Jev recommendations use a separate OpenRouter API key. No build or npm install is needed.
Choose either method.
Paste this into a Claude Code terminal session:
/plugin install jevshift --marketplace unclecode/jevshift
Confirm the source and choose User to make the plugin available across your projects. Claude Code may initially select Project scope.
claude plugin marketplace add unclecode/jevshift
claude plugin install jevshift@jevshift --scope user
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --effort low
Inside Claude Code, open the plugin settings:
/plugin configure jevshift@jevshift
Enter your OpenRouter API key in the masked field. To try automatic switching, turn on Enable experimental auto control. Save, exit Claude, then start it with:
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --effort low
Use that launch command for JevShift sessions. The restart loads your settings and enables the experimental hooks this alpha needs. --effort low sets the starting native effort; auto can choose any supported level. Manual pins and off work without a Jev key.
First, discover your available models:
/jevshift setup
Send a task, then check what Jev recommended and which model answered:
/jevshift status
To let JevShift apply its recommendations:
/jevshift auto
Try planning a small feature, implementing the agreed plan, then updating its documentation. Use status after each phase to see the returned model and requested effort.
| Command | What it does |
|---|---|
/jevshift status | Show recommendations, timing, requested effort and requested/returned model. |
/jevshift setup | Discover models and return to observe mode. |
/jevshift refresh | Refresh model versions while preserving your mode and pins. |
/jevshift observe | Recommend model and effort without applying them. |
/jevshift auto | Choose both automatically when auto is enabled in settings. |
/jevshift pin sonnet | Hold the Sonnet family with native effort; its version follows discovery. No Jev calls. Also accepts opus, fable or an exact discovered model ID. |
/jevshift pin opus high | Hold both model and effort. No Jev calls. |
/jevshift effort medium | Fix effort while keeping the current model policy. In observe, this stays advisory. |
/jevshift effort auto | Let Jev choose effort, including with a pinned model. Requires auto enabled to apply changes. |
/jevshift effort native | Keep Claude's native effort while retaining the model policy. |
/jevshift off | Return control to Claude Code. |
Changes apply to the next model request. Each session keeps its own controls. Changing the native model or effort turns JevShift off; /jevshift off always releases both. See independent pin examples.
Observe, auto and a pinned model with automatic effort send short conversation, plan and tool excerpts to OpenRouter / TypeSafe Jev. Known secret patterns are masked, but private content can still be included. Fully manual pins and off make no Jev calls. See what gets sent and a complete synthetic request.
This is an early preview. Evaluator calls can add delay or time out, and choices still need broader testing. A failed evaluation keeps native values for automatic choices and preserves valid manual pins. Effort is a reasoning preference, not a token budget. We have tested switching and controls, but have not established better task outcomes, cache preservation or quota savings. See validation and compatibility.
A screen-recorded walkthrough is coming soon.
| Folder | Contents |
|---|---|
.claude-plugin/ | Plugin metadata and marketplace listing. |
hooks/ | Claude Code integration. |
src/ | Context collection, Jev calls, model selection and controls. |
tests/ | Offline tests. |
examples/ | Synthetic request and sample settings. |
docs/ | Setup guides, compatibility and test results. |
I'm UncleCode. Follow me for updates and more tools from my workflow.
I also build Crawl4AI, an open-source web crawler for AI applications with 84K GitHub stars. If you work with web data, take a look.
MIT license. An independent project, not affiliated with Anthropic, TypeSafe or OpenRouter.
hooks/register.ts 4 lines1import type {Register} from 'claude-code';
2import {registerControlHooks} from '../src/controls-hooks.ts';
3export const register:Register=(on,options)=>registerControlHooks(on,options);
4src/controls-hooks.ts 53 lines1import type {Register} from 'claude-code';
2import {SessionControls} from './controls.ts';
3import {registerContextHooks} from './context-hooks.ts';
4import {discoverCatalog} from './discovery.ts';
5import type {Catalog,RoutingEvent} from './auto.ts';
6import type {Observation} from './observe.ts';
7import {isEffort} from './effort.ts';
8
9export function registerControlHooks(on:Parameters<Register>[0],options:Parameters<Register>[1],
10 record:(event:Observation|RoutingEvent)=>void=()=>{}):void {
11 let diagnostic:(event:Observation|RoutingEvent)=>void=()=>{};
12 const controls=new SessionControls(options.experimental_auto===true,event=>{record(event);diagnostic(event)},isEffort(options.max_effort)?options.max_effort:'max');
13 let discover:(()=>Promise<Catalog>)|undefined;
14 // No prompt is submitted. Safe mode prevents this helper loading this plugin again.
15 on('session.start',async($,e,next)=>{
16 discover=()=>discoverCatalog((argv,init)=>$.process.run(argv,init),
17 typeof options.claude_executable==='string'&&options.claude_executable.trim()?options.claude_executable:'claude');
18 await controls.bind(await $.session.id(),'startup',{get:key=>$.store.get(key),set:(key,value)=>$.store.set(key,value),discover:()=>discover!()});
19 await controls.warmCatalog();
20 await $.command.register({name:'jevshift',description:'Control Jev model and effort selection for this session',argumentHint:'status|setup|refresh|observe|auto|off|pin <model> [effort]|effort <level|auto|native>',immediate:true});
21 return next(e);
22 });
23 on('command.run',async($,e,next)=>{
24 if(e.command==='effort'){
25 if(isEffort(e.args.trim())||['auto','ultracode'].includes(e.args.trim()))await controls.nativeEffortCommand();
26 return next(e);
27 }
28 if(e.command!=='jevshift')return next(e);
29 await controls.bind(await $.session.id(),'command',{get:key=>$.store.get(key),set:(key,value)=>$.store.set(key,value),discover:()=>discover!()});
30 const text=await controls.command(e.args,await $.session.model());
31 $.ui.status(`JevShift · ${controls.mode}${controls.pin?` ${controls.pin}`:''} · effort ${controls.effort}${controls.blocked?' (blocked)':''}`);
32 return {text};
33 });
34 registerContextHooks(on,async(snapshot,id,host)=>{
35 diagnostic=event=>host.ui.log(JSON.stringify(event),{to:'debug'});
36 return controls.select(snapshot,id,{
37 now:()=>host.clock.now(),after:(ms,fn)=>host.clock.after(ms,fn),fetch:(url,init)=>host.http.fetch(url,init),
38 key:async()=>typeof options.openrouter_api_key==='string'&&options.openrouter_api_key.trim()
39 ?options.openrouter_api_key:options.use_environment_key===true?await host.environmentKey():undefined,
40 show:text=>{host.ui.status(text);host.ui.log(text)},
41 });
42 },()=>controls.invalidate(),{
43 lifecycle:async(id,source,store)=>{await controls.bind(id,source,{...store,discover:()=>discover!()})},
44 guard:id=>controls.guard(id),
45 finalize:(selection,id)=>controls.finalize(selection||undefined,id),
46 });
47 on('classic.PostModelSwitch',async($,e,next)=>{
48 await controls.nativeSwitch(e.source,e.to_model);
49 $.ui.status(`JevShift · ${controls.mode}${controls.blocked?' (blocked)':''}`);
50 return next(e);
51 });
52}
53src/controls.ts 250 lines1import {AutoSession,type Catalog,type RoutingEvent} from './auto.ts';
2import {ObserveSession,type ObserveHost,type Observation} from './observe.ts';
3import {compatible,resolveTarget,modelId,isTier,TIERS} from './catalog.ts';
4import type {Boundary,ModelSelection} from './context-hooks.ts';
5import type {ContextSnapshot} from './context.ts';
6import {isEffort,withinCap,type Effort,type EffortPolicy} from './effort.ts';
7export type Mode='observe'|'auto'|'off'|'pin';
8export type SavedControl={version:1;sessionId:string;mode:Mode;pin?:string;effort?:EffortPolicy;blocked?:boolean};
9export type ControlHost={get:(key:string)=>Promise<unknown>;set:(key:string,value:unknown)=>Promise<void>;discover:()=>Promise<Catalog>};
10const key=(id:string)=>`session-v1:${id}`;
11const help='Use /jevshift status, setup, refresh, observe, auto, off, pin <model> [effort], or effort <low|medium|high|xhigh|max|auto|native>.';
12export class SessionControls {
13 mode:Mode='observe';pin:string|undefined;blocked=false;catalog:Catalog={};sessionId='';
14 nativeModel='unknown';recommended:string|undefined;requested:string|undefined;actual:string|undefined;
15 effort:EffortPolicy='auto';nativeEffort:Boundary['effort'];recommendedEffort:Effort|undefined;requestedEffort:Boundary['effort'];
16 private nativeEffortSeen=false;
17 note='';private revision=0;private loaded=false;private loading:Promise<void>|undefined;
18 private catalogPending:Promise<Catalog>|undefined;private saveQueue:Promise<void>=Promise.resolve();
19 private catalogEpoch=0;
20 private host:ControlHost|undefined;private auto:AutoSession|undefined;
21 private observer:ObserveSession;
22 private lastEvaluation:Observation|undefined;
23 constructor(private allowAuto=false,private record:(event:Observation|RoutingEvent)=>void=()=>{},private maxEffort:Effort='max') {
24 this.observer=new ObserveSession(e=>{
25 if(e.event==='finished'&&e.sessionId===this.sessionId&&e.outcome!=='stale'&&e.outcome!=='aborted'){
26 this.lastEvaluation=e;if(e.recommendation){this.recommended=e.recommendation;this.recommendedEffort=e.effortRecommendation;}
27 }
28 this.record(e);
29 });
30 }
31 invalidate():void{this.revision++;this.observer.invalidate();this.auto?.invalidate();}
32 private change(mode:Mode,pin?:string,effort:EffortPolicy=mode==='pin'||mode==='off'?'native':'auto'):void {
33 this.invalidate();this.auto?.stop();this.auto=undefined;this.observer.invalidate(true);
34 this.mode=mode;this.pin=pin;this.effort=effort;this.blocked=false;this.note='';this.recommended=undefined;this.lastEvaluation=undefined;
35 this.requested=undefined;this.requestedEffort=undefined;this.recommendedEffort=undefined;
36 }
37 async bind(id:string,source:string,host:ControlHost):Promise<void>{
38 if(this.sessionId===id&&this.loaded&&source!=='clear'&&source!=='fork'){this.host=host;return;}
39 if(this.sessionId===id&&this.loading)return this.loading;
40 this.change('observe');this.sessionId=id;this.host=host;this.loaded=false;
41 this.actual=undefined;this.nativeModel='unknown';this.nativeEffort=undefined;this.nativeEffortSeen=false;this.catalog={};this.catalogPending=undefined;this.catalogEpoch++;
42 const revision=this.revision;
43 const pending=(async()=>{
44 let saved:unknown,readFailed=false;
45 try{saved=source==='clear'||source==='fork'?undefined:await host.get(key(id));}
46 catch{readFailed=true;}
47 if(this.sessionId!==id||this.revision!==revision)return;
48 if(readFailed)this.note='Saved mode could not be read; observe selected.';
49 const s=saved as Partial<SavedControl>|undefined;
50 if(s?.version===1&&s.sessionId===id&&['observe','off','auto','pin'].includes(s.mode??'')){
51 const effort=isEffort(s.effort)||s.effort==='auto'||s.effort==='native'?s.effort:undefined;
52 if((s.mode==='auto'||(s.mode==='pin'&&effort==='auto'))&&!this.allowAuto){this.change('off');this.note='Saved auto mode is unavailable until experimental auto is enabled.';}
53 else if(s.mode==='pin'&&(isTier(s.pin)||modelId(s.pin)||(s.pin===undefined&&effort!==undefined))){this.change('pin',s.pin,effort);this.blocked=s.blocked===true;}
54 else if(s.mode!=='pin')this.change(s.mode as Mode,undefined,effort);
55 }
56 this.loaded=true;
57 })();
58 this.loading=pending;
59 try{await pending}finally{if(this.loading===pending)this.loading=undefined}
60 }
61 private async discover():Promise<Catalog>{
62 if(Object.keys(this.catalog).length)return this.catalog;
63 const session=this.sessionId,epoch=this.catalogEpoch;
64 if(!this.catalogPending)this.catalogPending=this.host!.discover();
65 const pending=this.catalogPending;
66 try{const catalog=await pending;if(this.sessionId!==session)throw Error('Session changed');
67 if(epoch===this.catalogEpoch)this.catalog=this.capped(catalog);return this.catalog;}
68 finally{if(this.catalogPending===pending)this.catalogPending=undefined;}
69 }
70 private capped(catalog:Catalog):Catalog{
71 const result:Catalog={};for(const tier of TIERS){const target=catalog[tier];if(!target)continue;
72 const efforts=target.efforts.filter((e):e is Effort=>isEffort(e)&&withinCap(e,this.maxEffort));
73 const versions=target.versions?.map(v=>({model:v.model,efforts:v.efforts.filter(e=>isEffort(e)&&withinCap(e,this.maxEffort))})).filter(v=>v.efforts.length);
74 if(efforts.length)result[tier]={model:target.model,efforts,...(versions?.length?{versions}:{})};}return result;
75 }
76 async warmCatalog():Promise<void>{
77 const revision=this.revision;
78 try{await this.discover()}catch{if(this.revision===revision)this.note='Model discovery unavailable; /jevshift refresh retries. Native control is unchanged.';}
79 }
80 async save():Promise<void>{
81 if(!this.host||!this.loaded)return;
82 const host=this.host,id=this.sessionId;
83 const value:SavedControl={version:1,sessionId:id,mode:this.mode,effort:this.effort,...(this.pin?{pin:this.pin}:{}),...(this.blocked?{blocked:true}:{})};
84 this.saveQueue=this.saveQueue.catch(()=>{}).then(()=>host.set(key(id),value));
85 try{await this.saveQueue}catch{if(this.sessionId===id)this.note='Mode changed for this process, but could not be saved for resume.';}
86 }
87 async command(args:string,native:string):Promise<string>{
88 this.nativeModel=native;
89 const words=args.trim().split(/\s+/).filter(Boolean),action=words[0]??'status';
90 if(action==='status'&&words.length<=1)return this.status();
91 if(['setup','refresh'].includes(action)&&words.length===1){
92 const revision=this.revision;
93 try{
94 const discovered=await this.host!.discover();
95 if(revision!==this.revision)return 'Discovery discarded because controls changed.';
96 const blocked=this.blocked,note=this.note;
97 this.catalog=this.capped(discovered);this.catalogEpoch++;
98 this.change(action==='setup'?'observe':this.mode,action==='setup'?undefined:this.pin,action==='setup'?'auto':this.effort);
99 if(action==='refresh'){this.blocked=blocked;this.note=blocked?note:'';}
100 await this.save();
101 return `Models discovered: ${TIERS.filter(t=>this.catalog[t]).map(t=>`${t} = ${this.catalog[t]!.model} (${this.catalog[t]!.efforts.join(', ')})`).join('; ')}. ${action==='setup'?'Observe selected.':'Current controls preserved.'} Automatic effort cap: ${this.maxEffort}.`;
102 }catch{return 'Model discovery failed; existing mode unchanged. Check that the configured Claude executable is available.';}
103 }
104 if(['off','observe','auto'].includes(action)&&words.length===1){
105 if(action==='auto'){
106 if(!this.allowAuto)return 'Auto is still experimental and disabled. Selection quality and real-world evaluator reliability remain under review; enable experimental_auto only for an isolated development test.';
107 const revision=this.revision;
108 try{await this.discover()}catch{return 'Model discovery failed; existing mode unchanged.';}
109 if(this.revision!==revision)return 'Auto request discarded because controls changed.';
110 }
111 this.change(action as Mode);await this.save();return this.status();
112 }
113 if(action==='pin'&&(words.length===2||words.length===3)){
114 if(words.length===3&&(!isEffort(words[2])||!withinCap(words[2],this.maxEffort)))return 'Invalid or capped effort; existing mode unchanged. '+help;
115 if(!TIERS.includes(words[1] as any)&&!modelId(words[1]))return 'Invalid pin; existing mode unchanged. '+help;
116 const revision=this.revision;
117 try{await this.discover()}catch{return 'Model discovery failed; existing mode unchanged.';}
118 if(this.revision!==revision)return 'Pin request discarded because controls changed.';
119 const target=resolveTarget(this.catalog,words[1],native);
120 if(!target)return 'That model is not in the supported native catalog; existing mode unchanged.';
121 if(!compatible(target.target,native,words[2]))return 'That pin is incompatible with the selected context window or supported effort; existing mode unchanged.';
122 this.change('pin',words[1],words[2] as Effort|undefined);await this.save();return this.status();
123 }
124 if(action==='effort'&&words.length===2){
125 const effort=words[1];
126 if(!isEffort(effort)&&effort!=='auto'&&effort!=='native')return 'Invalid effort; existing mode unchanged. '+help;
127 if(isEffort(effort)&&!withinCap(effort,this.maxEffort))return `Effort exceeds the configured ${this.maxEffort} cap; existing mode unchanged.`;
128 if(effort==='auto'&&!this.allowAuto&&this.mode!=='observe')return 'Automatic effort is experimental; enable experimental_auto first.';
129 const revision=this.revision;
130 try{await this.discover()}catch{return 'Model discovery failed; existing mode unchanged.';}
131 if(revision!==this.revision)return 'Effort request discarded because controls changed.';
132 if(this.mode==='pin'&&isEffort(effort)){
133 const target=resolveTarget(this.catalog,this.pin??native,native);
134 if(!target||!compatible(target.target,native,effort))return 'That effort is unavailable for the pinned model; existing mode unchanged.';
135 }
136 // Observe remains advisory. Off plus a fixed effort holds the native model without calling Jev.
137 const mode=this.mode==='off'?(effort==='native'?'off':'pin'):this.mode;
138 this.change(mode,this.pin,effort);await this.save();return this.status();
139 }
140 return help+' Existing mode unchanged.';
141 }
142 status():string{
143 const evaluation=this.lastEvaluation;
144 const outcome=evaluation?({transport_timeout:'timed out',timeout:'timed out',missing_key:'missing key',
145 transport_disabled:'disabled by native network setting',recommended:'recommended'} as Record<string,string>)[evaluation.outcome]??evaluation.outcome:'none';
146 const evaluationText=outcome+(evaluation?.elapsedMs!==undefined?` (${evaluation.elapsedMs} ms)`:'');
147 return [`JevShift: ${this.mode}${this.pin?` (${this.pin})`:''}${this.blocked?' (blocked; choose another model or /jevshift off)':''}`,
148 `Native default: ${this.nativeModel}`,`Native effort: ${this.nativeEffort??'default'}`,`Effort control: ${this.effort} (cap: ${this.maxEffort})`,
149 `Resolved models: ${TIERS.filter(t=>this.catalog[t]).map(t=>`${t} = ${this.catalog[t]!.model}`).join('; ')||'not discovered'}`,
150 `Last Jev result: ${evaluationText}`,`Last Jev recommendation: ${this.recommended??'none'}`,`Last Jev effort: ${this.recommendedEffort??'none'}`,
151 `Last requested model: ${this.requested??'none'}`,`Last returned model: ${this.actual??'none'}`,
152 `Last requested effort: ${this.requested===undefined?'none':this.requestedEffort??'default'}`,
153 ...(this.note?[this.note]:[]),...(this.mode==='pin'?[this.effort==='auto'?'Model held; Jev chooses effort.':'Pin makes no Jev calls. Changes apply on the next model request.']:[])].join('\n');
154 }
155 async nativeEffortCommand():Promise<void>{this.change('off');this.note='Native effort selection took control.';await this.save();}
156 private fallback(id:Boundary):ModelSelection{
157 const pinned=this.mode==='pin',fixed=isEffort(this.effort)&&!['off','observe'].includes(this.mode);
158 const input=pinned?this.pin??id.nativeModel:id.nativeModel,effort=fixed?this.effort as Effort:id.effort;
159 let model=input;
160 if(pinned||fixed){
161 const target=resolveTarget(this.catalog,model,id.nativeModel);
162 if(!target||!compatible(target.target,id.nativeModel,effort))return {model:id.nativeModel,block:'The held model/effort pair is unavailable. Choose another pin or use /jevshift off.'};
163 model=target.target.model;
164 }
165 return {model,effort,...(pinned?{stopFallback:true}:{})};
166 }
167 async nativeSwitch(source:string,model:string):Promise<void>{
168 this.nativeModel=model;
169 if(['command','picker','sdk'].includes(source)){this.change('off');this.note='Native model selection took control.';await this.save();}
170 else if(source==='auto'){
171 if(this.mode==='pin'){this.invalidate();this.blocked=true;this.note='Native fallback attempted; pin remains blocked.';await this.save();}
172 else if(this.mode==='auto'){this.change('off');this.note='Native fallback stopped automatic selection.';await this.save();}
173 }
174 }
175 async guard(id:Boundary):Promise<ModelSelection|undefined>{
176 if(id.sessionId!==this.sessionId)return;
177 this.nativeModel=id.nativeModel;
178 if(this.nativeEffortSeen&&this.nativeEffort!==id.effort&&this.mode!=='off'){
179 this.change('off');this.note='Native effort changed; native control retained.';await this.save();
180 }
181 this.nativeEffort=id.effort;this.nativeEffortSeen=true;
182 if(this.mode==='off')return this.track({model:id.nativeModel,effort:id.effort},id);
183 if(this.mode!=='pin')return;
184 if(!this.blocked){
185 const revision=this.revision;
186 try{await this.discover()}catch{if(revision===this.revision){this.blocked=true;this.note='Pinned model could not be revalidated.';}}
187 if(revision!==this.revision)return this.guard(id);
188 const target=resolveTarget(this.catalog,this.pin??id.nativeModel,id.nativeModel);
189 const effort=this.effort==='auto'?undefined:this.effort==='native'?id.effort:this.effort;
190 if(!target||!compatible(target.target,id.nativeModel,effort)){this.blocked=true;this.note='Pinned model is unavailable or incompatible with this effort/context setting.';}
191 }
192 if(this.blocked){await this.save();return {model:id.nativeModel,block:this.note||'Pinned model is blocked. Choose a model or /jevshift off.'};}
193 if(this.effort==='auto')return;
194 return this.track({model:resolveTarget(this.catalog,this.pin??id.nativeModel,id.nativeModel)!.target.model,effort:this.effort==='native'?id.effort:this.effort,stopFallback:true},id,true);
195 }
196 async select(snapshot:ContextSnapshot,id:Boundary,host:ObserveHost):Promise<ModelSelection|undefined>{
197 if(id.sessionId!==this.sessionId)return;
198 const guarded=await this.guard(id);if(guarded)return guarded;
199 const revision=this.revision;
200 let selected:ModelSelection|undefined;
201 if(this.mode==='auto'||(this.mode==='pin'&&this.effort==='auto')){
202 try{await this.discover()}catch{if(revision===this.revision){this.change('off');this.note='Model discovery failed; native control retained.';await this.save();}return;}
203 if(revision!==this.revision)return this.guard(id);
204 const pinned=this.mode==='pin'?resolveTarget(this.catalog,this.pin??id.nativeModel,id.nativeModel):undefined;
205 const catalog=pinned?{[pinned.tier]:pinned.target}:this.catalog;
206 this.auto??=new AutoSession(catalog,e=>this.record(e),this.observer);
207 // A restored auto controller is rebuilt after current-process discovery.
208 selected=await this.auto.select(snapshot,id,host,{...(pinned?{pinnedModel:pinned.tier}:{}),
209 ...(this.effort!=='auto'?{fixedEffort:this.effort}:{}),nativeEffort:id.effort});
210 if(pinned&&!selected)selected=this.fallback(id);
211 }else {
212 try{await this.discover()}catch{this.note='Model discovery failed; recommendation skipped.';return this.track({model:id.nativeModel,effort:id.effort},id);}
213 if(revision!==this.revision)return this.guard(id);
214 await this.observer.consume(snapshot,id,host,{scope:{catalog:this.catalog,...(this.effort!=='auto'?{fixedEffort:this.effort}:{}),nativeEffort:id.effort}});
215 }
216 if(this.revision!==revision)return this.guard(id);
217 return this.track(selected?{...selected,...(this.mode==='pin'?{stopFallback:true}:{})}:this.fallback(id),id,this.mode==='pin',this.mode==='auto');
218 }
219 async finalize(selected:ModelSelection|undefined,id:Boundary):Promise<ModelSelection|undefined>{
220 if(id.sessionId!==this.sessionId)return;
221 // An immediate pin/off command can supersede a decision while Jev is awaited.
222 if((this.mode==='pin'&&this.effort!=='auto')||this.mode==='off')return this.guard(id);
223 if((this.mode==='pin'||this.mode==='auto')&&!selected)return this.track(this.fallback(id),id,this.mode==='pin',this.mode==='auto');
224 if(selected?.valid&&!selected.valid())return this.track(this.fallback(id),id,this.mode==='pin',this.mode==='auto');
225 return selected;
226 }
227 private track(selection:ModelSelection,id:Boundary,pinned=false,automatic=false):ModelSelection{
228 const revision=this.revision,session=this.sessionId;
229 this.requested=selection.model;this.requestedEffort=selection.effort;
230 return {...selection,valid:()=>revision===this.revision&&!id.signal?.aborted&&(!selection.valid||selection.valid()),
231 completed:async actual=>{
232 if(session!==this.sessionId)return;
233 await selection.completed?.(actual);this.actual=actual??undefined;
234 if(revision!==this.revision)return;
235 if(actual&&actual!==selection.model.replace('[1m]','')){
236 if(pinned){this.blocked=true;this.note='Returned model differed from the pin; further requests blocked.';}
237 else if(automatic){this.change('off');this.note='Returned model differed; native control retained.';}
238 await this.save();
239 }
240 },
241 failed:async error=>{
242 await selection.failed?.(error);
243 if(revision!==this.revision)return;
244 if(pinned){this.blocked=true;this.note='Pinned model request failed. Choose another model, re-pin to retry, or use /jevshift off.';}
245 else if(automatic){this.change('off');this.note='Native request failed; automatic selection stopped.';}
246 await this.save();
247 }};
248 }
249}
250src/context-hooks.ts 105 lines1import type { Register, EngineInterface, TurnStepInput } from 'claude-code';
2import type {Reply} from './jev.ts';
3import { boundedFetch,TransportError } from './transport.ts';
4import { ContextSession } from './context-session.ts';
5import type { ContextSnapshot } from './context.ts';
6import { routingEvidence, type ToolOutcome } from './routing-evidence.ts';
7
8/** Collect at native request boundaries; only an explicit validated selection rewrites a model. */
9export type Boundary = {sessionId: string; turnId: string; step: number; nativeModel: string; effort?: TurnStepInput['effort']; signal?: AbortSignal; outcomes?: ToolOutcome[]};
10export type Invalidation = 'prompt' | 'turn' | 'session' | 'compact';
11export type ModelSelection = {model:string; effort?: TurnStepInput['effort']; valid?:()=>boolean; block?:string; stopFallback?:boolean; completed?:(actual:string|null)=>void|Promise<void>; failed?:(error?:unknown)=>void|Promise<void>};
12export type BoundaryHost = {
13 clock: Pick<EngineInterface['clock'],'now'|'after'>;
14 http: {fetch:(url:string,init:{method:string;headers:Record<string,string>;body:string})=>Promise<Reply>};
15 ui: Pick<EngineInterface['ui'],'log'|'status'>;
16 environmentKey: () => Promise<string | undefined>;
17};
18export type ContextControls = {
19 lifecycle?:(id:string,source:string,host:{get:(key:string)=>Promise<unknown>;set:(key:string,value:unknown)=>Promise<void>})=>Promise<void>;
20 guard?:(id:Boundary)=>Promise<ModelSelection|undefined>;
21 finalize?:(selection:void|ModelSelection,id:Boundary)=>Promise<ModelSelection|undefined>;
22};
23export function registerContextHooks(on: Parameters<Register>[0], consume: (snapshot: ContextSnapshot,
24 identity: Boundary, host: BoundaryHost) => Promise<void | ModelSelection>, invalidate: (reason:Invalidation) => void = () => {}, controls:ContextControls = {}): void {
25 const context = new ContextSession();
26 on('prompt.submit', async ($, e, next) => {
27 let ticket: number | null = null;
28 try {
29 ticket = context.stage(await $.session.id(), e, await $.session.messages());
30 if (ticket !== null) invalidate('prompt');
31 } catch { /* Native prompt still proceeds. */ }
32 try {
33 const result = await next(e);
34 if ('drop' in result) context.reject(ticket);
35 return result;
36 } catch (error) { context.reject(ticket); throw error; }
37 });
38 on('turn.start', async ($, e, next) => {
39 invalidate('turn');
40 try { context.beginTurn(await $.session.id(), e.turnId, e.text); } catch { context.reset(); }
41 return next(e);
42 });
43 on('classic.SessionStart', async ($, e, next) => {
44 if (['clear','fork','resume'].includes(e.source)) { context.reset(); invalidate('session'); }
45 await controls.lifecycle?.(await $.session.id(),e.source,{get:key=>$.store.get(key),set:(key,value)=>$.store.set(key,value)});
46 return next(e);
47 });
48 on('session.compact', async ($, e, next) => {
49 const result = await next(e);
50 if (!e.agentId && e.trigger !== 'precompute' && !result.skip) { context.compacted(); invalidate('compact'); }
51 return result;
52 });
53 on('turn.step', async function* ($, e, next) {
54 let selection: void | ModelSelection;
55 let identity:Boundary|undefined;
56 if (!e.agentId) {
57 // A collection/evaluator failure preserves native behavior and does not retry.
58 try {
59 const id = await $.session.id();
60 identity={sessionId:id,turnId:e.turnId,step:e.index,nativeModel:e.model,effort:e.effort,signal:next.signal};
61 selection=await controls.guard?.(identity);
62 if(!selection){
63 const messages = await $.session.messages();
64 const snapshot = context.snapshot(id, e.turnId, messages);
65 selection = await consume(snapshot, {sessionId:id, turnId:e.turnId, step:e.index, nativeModel:e.model, effort:e.effort,
66 outcomes:routingEvidence(messages),signal:next.signal}, {
67 clock:{now:()=>$.clock.now(),after:(ms,fn)=>$.clock.after(ms,fn)},
68 http:{fetch:async(url,init)=>{
69 const disabled=await $.env.get('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC');
70 if(disabled==='1'||disabled==='true')throw new TransportError('transport_disabled');
71 return boundedFetch((argv,options)=>$.process.run(argv,options),url,init);
72 }},
73 ui:{log:(text,options)=>$.ui.log(text,options),status:text=>$.ui.status(text)},
74 environmentKey:()=>$.env.get('OPENROUTER_API_KEY'),
75 });
76 }
77 } catch { /* A collector failure leaves native behavior intact. */ }
78 }
79 if(identity&&controls.finalize)selection=await controls.finalize(selection,identity);
80 if(selection?.block){
81 $.ui.log(`JevShift · ${selection.block}`);$.ui.status('JevShift · Pin blocked');
82 await $.turn.abort({turnId:e.turnId});
83 return {turnId:e.turnId,index:e.index,answer:'',toolUses:[],stopReason:null,usage:null};
84 }
85 // Only model and effort are rewritable. Result and stream are returned verbatim.
86 if (selection?.valid && !selection.valid()) selection=undefined;
87 try {
88 const result = yield* next(selection ? {...e,model:selection.model,effort:selection.effort} : e);
89 try {
90 if (result?.stopReason === null && !result.usage) await selection?.failed?.();
91 else await selection?.completed?.(result?.usage?.model ?? null);
92 } catch { /* Diagnostic only. */ }
93 return result;
94 } catch (error) {
95 try { await selection?.failed?.(error); } catch { /* Preserve the original native error. */ }
96 if(selection?.stopFallback&&String(error).includes('Model fallback triggered:')){
97 $.ui.log('JevShift · Pinned model unavailable; automatic fallback stopped. Choose another model or /jevshift off.');
98 await $.turn.abort({turnId:e.turnId});
99 return {turnId:e.turnId,index:e.index,answer:'',toolUses:[],stopReason:null,usage:null};
100 }
101 throw error;
102 }
103 });
104}
105src/discovery.ts 20 lines1import {CATALOG_ARGS,CATALOG_INPUT,parseCatalog,TIERS} from './catalog.ts';
2import type {Catalog} from './auto.ts';
3
4type Runner=(argv:string[],init:{stdin:string;timeoutMs:number;env:Record<string,string>})=>Promise<{exitCode:number;stdout:string}>;
5/** Isolated no-inference helpers inherit the user's provider configuration and model overrides. */
6export async function discoverCatalog(run:Runner,executable:string):Promise<Catalog>{
7 const results=await Promise.allSettled(TIERS.map(async tier=>{
8 const result=await run([executable,...CATALOG_ARGS,'--model',tier],{
9 stdin:CATALOG_INPUT,timeoutMs:8000,
10 env:{CLAUDECODE:'',CLAUDE_CODE_ENABLE_FUNCTION_HOOKS:'0',CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:'1'},
11 });
12 if(result.exitCode!==0)throw Error('Catalog process failed');
13 return parseCatalog(result.stdout,tier);
14 }));
15 const catalog:Catalog={};
16 results.forEach((r,i)=>{if(r.status==='fulfilled')catalog[TIERS[i]]=r.value});
17 if(!Object.keys(catalog).length)throw Error('No supported model aliases');
18 return catalog;
19}
20src/auto.ts 105 lines1import type { Boundary, ModelSelection } from './context-hooks.ts';
2import type { ContextSnapshot } from './context.ts';
3import { ObserveSession, type ObserveHost, type Observation } from './observe.ts';
4import type { Tier,DecisionScope } from './jev.ts';
5import {contextTarget} from './catalog.ts';
6
7export type Target = {model:string; efforts:readonly string[]; versions?:readonly Target[]};
8/** Supplied by the host after native capability discovery; no inferred account entitlement. */
9export type Catalog = Partial<Record<Tier,Target>>;
10export type RoutingEvent = {event:'routing';sessionId:string;turnId:string;step:number;generation:number;
11 outcome:string;reason?:string;incomingModel:string;selectedModel?:string;selectedEffort?:Boundary['effort'];actualModel?:string|null;choice?:Tier;evaluations:number};
12export const AUTO_LIMITS = {evaluations:3, progressSteps:2, progressResults:2, repeatedErrors:2} as const;
13type Picked = {tier:Tier;model:string;effort:Boundary['effort']};
14export class AutoSession {
15 private evaluator: ObserveSession;
16 private session=''; private instruction=-1; private generation=-1;
17 private enabled=true; private epoch=0; private evaluations=0;
18 private selected: Picked|undefined; private visited=new Set<string>();
19 private seenOutcomes=new Set<string>(); private boundary=''; private boundaryCount=0; private evaluatedAt=0;
20 private notice='';
21 constructor(private readonly catalog: Catalog, private readonly record:(e:Observation|RoutingEvent)=>void=()=>{}, evaluator?:ObserveSession) {
22 this.evaluator=evaluator??new ObserveSession(e=>this.emit(e));
23 }
24 private emit(e:Observation|RoutingEvent):void { try {this.record(e)} catch { /* Diagnostics never select models. */ } }
25 invalidate():void {this.epoch++;this.evaluator.invalidate();this.selected=undefined;}
26 stop():void {this.enabled=false;this.invalidate();}
27 private show(host:ObserveHost,text:string):void {if(this.notice===text)return;this.notice=text;try{host.show(text)}catch{}}
28 private supported(tier:Tier,id:Boundary,effort:Boundary['effort']):Picked|undefined {
29 const offered=this.catalog[tier],target=offered&&contextTarget(offered,id.nativeModel);
30 if (!target || !/^claude-[a-z0-9-]+(?:\[1m\])?$/.test(target.model)) return;
31 // A catalog entry is capability evidence, never a quota/entitlement bypass.
32 if (effort !== undefined && (typeof effort!=='string' || !target.efforts.includes(effort))) return;
33 if (id.nativeModel.includes('[1m]') && !target.model.includes('[1m]')) return;
34 return {tier,model:target.model,effort};
35 }
36 async select(snapshot:ContextSnapshot,id:Boundary,host:ObserveHost,scope:DecisionScope={}):Promise<ModelSelection|undefined> {
37 if (!this.enabled || id.signal?.aborted || !snapshot.state || !snapshot.meta.ready) return;
38 if (this.session && this.session!==id.sessionId) {this.stop();return;}
39 this.session=id.sessionId;
40 const event=(outcome:string,extra:Partial<RoutingEvent>={})=>this.emit({event:'routing',sessionId:id.sessionId,
41 turnId:id.turnId,step:id.step,generation:snapshot.meta.generation,incomingModel:id.nativeModel,
42 evaluations:this.evaluations,outcome,...extra});
43 const instruction=snapshot.meta.instructionId;
44 const fresh=instruction!==this.instruction;
45 const revision=snapshot.meta.generation!==this.generation;
46 if (fresh) {
47 this.invalidate();this.instruction=instruction;this.evaluations=0;this.visited.clear();this.seenOutcomes.clear();
48 this.boundaryCount=0;this.evaluatedAt=0;this.boundary='';
49 } else if (revision) this.invalidate();
50 this.generation=snapshot.meta.generation;
51 const boundary=`${id.turnId}/${id.step}/${snapshot.meta.generation}`;
52 if (boundary!==this.boundary) {this.boundary=boundary;this.boundaryCount++;}
53 const outcomes=id.outcomes ?? [], newOutcomes=outcomes.filter(t=>!this.seenOutcomes.has(t.id));
54 let reason:string|undefined;
55 if (fresh || revision) reason=fresh?'new_instruction':'context_revision';
56 else if (newOutcomes.some(t=>t.tool==='ExitPlanMode'&&!t.error)) reason='plan_approved';
57 else if (newOutcomes.filter(t=>t.error).length>=AUTO_LIMITS.repeatedErrors) reason='repeated_tool_errors';
58 else if (this.boundaryCount-this.evaluatedAt>=AUTO_LIMITS.progressSteps && newOutcomes.length>=AUTO_LIMITS.progressResults) reason='new_tool_progress';
59 if (reason && this.evaluations<AUTO_LIMITS.evaluations) {
60 const previous=this.selected;this.selected=undefined;
61 const epoch=this.epoch;
62 this.evaluations++;this.evaluatedAt=this.boundaryCount;
63 this.seenOutcomes=new Set(outcomes.map(t=>t.id));
64 const result=await this.evaluator.consume(snapshot,id,host,{checkpoint:`${instruction}/${snapshot.meta.generation}/${this.evaluations}`,quiet:true,scope:{...scope,catalog:this.catalog,nativeEffort:id.effort}});
65 if (!this.enabled || this.epoch!==epoch || id.signal?.aborted) {event('stale');return;}
66 if (result?.outcome!=='recommended' || !result.recommendation) {
67 event('native_passthrough',{reason:result?.outcome ?? 'evaluator_skipped'});
68 this.show(host,'JevShift · Auto: no fresh Jev choice; keeping the incoming model and effort.');return;
69 }
70 const choice=result.recommendation,effort=scope.fixedEffort==='native'?id.effort:result.effortRecommendation,
71 target=this.supported(choice,id,effort);
72 if (!target) {
73 event('target_not_validated',{reason,choice});
74 this.show(host,'JevShift · Auto: target model or effort not validated; keeping the incoming model.');return;
75 }
76 // Reversals need fresh repeated failures, not another routine progress sample.
77 const pair=`${choice}/${effort??'default'}`;
78 if (previous && (choice!==previous.tier||effort!==previous.effort) && this.visited.has(pair) && !['repeated_tool_errors','plan_approved'].includes(reason)) {
79 this.selected=previous;event('reversal_held',{reason,choice,selectedModel:previous.model});
80 } else {
81 this.selected=target;this.visited.add(pair);event('choice_accepted',{reason,choice,selectedModel:target.model,selectedEffort:target.effort});
82 }
83 } else if (reason && this.evaluations>=AUTO_LIMITS.evaluations) event('evaluation_cap',{reason});
84 const selected=this.selected;
85 if (!selected) return;
86 // A reused choice must still fit this incoming request's effort/context mode.
87 const compatible=this.supported(selected.tier,id,selected.effort);
88 if (!compatible || compatible.model!==selected.model) {this.selected=undefined;event('incompatible_request');return;}
89 const epoch=this.epoch;
90 event('request_selected',{choice:selected.tier,selectedModel:selected.model,selectedEffort:selected.effort});
91 this.show(host,`JevShift · Auto: requesting ${selected.model} / ${selected.effort??'default'}.`);
92 return {model:selected.model,effort:selected.effort,
93 valid:()=>this.enabled && this.epoch===epoch && this.selected===selected && !id.signal?.aborted,
94 completed:actual=>{
95 event('response_observed',{selectedModel:selected.model,actualModel:actual});
96 if (epoch===this.epoch && actual && actual!==selected.model.replace('[1m]','')) {
97 this.stop();this.show(host,'JevShift · Auto stopped: Claude returned a different model. Native control retained.');
98 }
99 },
100 failed:()=>{if(epoch===this.epoch)this.stop();event('native_request_failed',{selectedModel:selected.model});
101 this.show(host,'JevShift · Auto stopped after a native request error. The original error remains visible.');},
102 };
103 }
104}
105src/observe.ts 122 lines1import {EVALUATOR_LIMITS} from './evaluator-limits.ts';
2import {TransportError,type TransportOutcome} from './transport.ts';
3import type { ContextSnapshot } from './context.ts';
4import type { Boundary } from './context-hooks.ts';
5import { ENDPOINT, parseReply, requestBody, type Reply, type Parsed, type Tier, type Usage,type DecisionScope } from './jev.ts';
6import type {Effort} from './effort.ts';
7
8export type ObserveHost = {
9 now: () => Promise<number>;
10 after: (ms: number, fn: () => void) => {cancel: () => void};
11 fetch: (url: string, init: {method:string;headers:Record<string,string>;body:string}) => Promise<Reply>;
12 key: () => Promise<string | undefined>;
13 show: (text: string) => void;
14};
15export type Observation = {
16 event: 'started' | 'finished' | 'late_settlement' | 'skipped';
17 sessionId: string; turnId: string; generation: number; step: number; attempt?: number;
18 outcome: string; nativeModel: string; recommendation?: Tier;effortRecommendation?:Effort;
19 timestampMs: number; elapsedMs?: number; billing?:'reported'|'unknown'|'not_requested'; usage?: Usage; httpStatus?: number; evaluatorModel?: string;
20};
21const safeModel = (x: string) => typeof x === 'string' && /^[\w./:-]{1,128}$/.test(x) ? x : 'selected model';
22const title = (s: string) => s.charAt(0).toUpperCase()+s.slice(1);
23
24/** One instance per registration. No model/effort mutation, raw-context log, or retries. */
25export class ObserveSession {
26 private session = '';
27 private seen = '';
28 private epoch = 0;
29 private serial = 0;
30 private pending = false;
31 private cancelWait: (() => void) | undefined;
32 private lastNotice = '';
33 constructor(private readonly record: (event: Observation) => void = () => {}) {}
34 invalidate(clearSeen = false): void { this.epoch++; this.cancelWait?.(); if (clearSeen) this.seen=""; }
35 private emit(e: Observation): void { try { this.record(e); } catch { /* Diagnostics cannot break native work. */ } }
36 private notice(host: ObserveHost, text: string): void {
37 if (this.lastNotice === text) return;
38 this.lastNotice = text;
39 try { host.show(text); } catch { /* UI is advisory. */ }
40 }
41 async consume(snapshot: ContextSnapshot, id: Boundary, host: ObserveHost,
42 options: {checkpoint?:string; quiet?:boolean;scope?:DecisionScope} = {}): Promise<Observation | undefined> {
43 if (this.session !== id.sessionId) { this.invalidate(); this.session=id.sessionId; this.seen=''; this.lastNotice=''; }
44 const key = options.checkpoint ?? String(snapshot.meta.generation);
45 if (key === this.seen) return;
46 this.seen = key;
47 this.invalidate();
48 const epoch = this.epoch;
49 const nativeModel = safeModel(id.nativeModel);
50 const started = await host.now();
51 const base = {sessionId:id.sessionId,turnId:id.turnId,generation:snapshot.meta.generation,step:id.step,nativeModel,timestampMs:started};
52 if (!snapshot.state || !snapshot.meta.ready || id.signal?.aborted) {
53 this.emit({...base,event:'skipped',outcome:'no_current_context'}); return;
54 }
55 if (this.pending) {
56 this.emit({...base,event:'skipped',outcome:'request_still_pending'});
57 if (!options.quiet) this.notice(host,'JevShift · Observe: previous Jev request still pending; selected model unchanged.'); return;
58 }
59 const body = requestBody(snapshot.state,options.scope);
60 if (!body) { this.emit({...base,event:'skipped',outcome:'payload_too_large'}); return; }
61 // Start the deadline before reading credentials. A local deadline does not cancel HTTP.
62 let wake: (value: 'timeout' | 'stale' | 'aborted') => void = () => {};
63 const stopped = new Promise<'timeout' | 'stale' | 'aborted'>(resolve => { wake=resolve; });
64 const cancel = () => wake('stale');
65 const abort = () => wake('aborted');
66 this.cancelWait = cancel;
67 const timer = host.after(EVALUATOR_LIMITS.selectionWaitMs,() => wake('timeout'));
68 id.signal?.addEventListener('abort',abort,{once:true});
69 this.pending = true;
70 let attempt: number | undefined;
71 let abandoned = false;
72 type Result = Parsed | {outcome:'missing_key' | 'network_error' | 'stale' | TransportOutcome};
73 const operation = (async (): Promise<Result> => {
74 try {
75 const credential = (await host.key())?.trim();
76 if (abandoned || epoch !== this.epoch || id.signal?.aborted) return {outcome:'stale'};
77 if (!credential || credential.length > 4096 || /[\r\n]/.test(credential)) return {outcome:'missing_key'};
78 attempt = ++this.serial;
79 this.emit({...base,event:'started',outcome:'requested',attempt});
80 const reply = await host.fetch(ENDPOINT,{method:'POST',headers:{Authorization:`Bearer ${credential}`,'Content-Type':'application/json'},body});
81 return parseReply(reply,options.scope);
82 } catch (error) { return {outcome:error instanceof TransportError?error.outcome:'network_error'}; }
83 })();
84 const settlement = operation.then(async result => {
85 this.pending = false;
86 if (abandoned) {
87 let now = started;
88 try { now = await host.now(); } catch { /* Preserve settlement accounting. */ }
89 this.emit({...base,event:'late_settlement',outcome:result.outcome,attempt,elapsedMs:now-started,
90 billing:('usage' in result && result.usage)?'reported':attempt&&result.outcome!=='transport_disabled'?'unknown':'not_requested',
91 ...('usage' in result ? {usage:result.usage,httpStatus:result.httpStatus} : {})});
92 }
93 return result;
94 });
95 let result: Result | 'timeout' | 'stale' | 'aborted';
96 try { result = await Promise.race([settlement,stopped]); }
97 finally {
98 timer.cancel(); id.signal?.removeEventListener('abort',abort);
99 if (this.cancelWait === cancel) this.cancelWait=undefined;
100 }
101 if (typeof result === 'string') abandoned = true;
102 const elapsedMs = Math.max(0,(await host.now())-started);
103 const stale = epoch !== this.epoch || id.signal?.aborted;
104 const outcome = stale ? 'stale' : typeof result === 'string' ? result : result.outcome;
105 const decision = typeof result === 'object' && 'decision' in result ? result.decision : undefined;
106 const observation: Observation = {...base,event:'finished',attempt,outcome,elapsedMs,
107 billing:typeof result==='object'&&'usage' in result&&result.usage?'reported':attempt&&!(typeof result==='object'&&result.outcome==='transport_disabled')?'unknown':'not_requested',
108 ...(!stale && outcome==='recommended' && decision ? {recommendation:decision.choice,effortRecommendation:decision.effort,evaluatorModel:decision.model} : {}),
109 ...(typeof result==='object' && 'httpStatus' in result ? {usage:result.usage,httpStatus:result.httpStatus} : {})};
110 this.emit(observation);
111 if (stale || outcome==='aborted' || outcome==='stale' || options.quiet) return observation;
112 if (outcome==='recommended' && decision) {
113 this.notice(host,`JevShift · Observe: recommend ${title(decision.choice)}${decision.effort?` / ${decision.effort}`:''}; keeping ${nativeModel} / ${id.effort??'default'}.`);
114 } else if (outcome==='missing_key') {
115 this.notice(host,'JevShift · Observe needs an OpenRouter key. Set the sensitive openrouter_api_key plugin option; selected model unchanged.');
116 } else {
117 this.notice(host,`JevShift · Observe: ${outcome==='timeout'||outcome==='transport_timeout' ? 'Jev request timed out' : 'Jev unavailable or reply invalid'}; selected model unchanged.`);
118 }
119 return observation;
120 }
121}
122src/effort.ts 6 lines1export const EFFORTS = ['low','medium','high','xhigh','max'] as const;
2export type Effort = typeof EFFORTS[number];
3export type EffortPolicy = Effort | 'auto' | 'native';
4export const isEffort = (value:unknown):value is Effort => typeof value==='string' && EFFORTS.includes(value as Effort);
5export const withinCap = (value:Effort, cap:Effort):boolean => EFFORTS.indexOf(value)<=EFFORTS.indexOf(cap);
6src/catalog.ts 61 lines1import type {Catalog,Target} from './auto.ts';
2import type {Tier} from './jev.ts';
3import {EFFORTS} from './effort.ts';
4export const TIERS:readonly Tier[]=['sonnet','opus','fable'];
5export const isTier=(x:unknown):x is Tier=>TIERS.includes(x as Tier);
6export const modelId=(x:unknown):x is string=>typeof x==='string'&&/^claude-[a-z0-9-]{1,100}(?:\[1m\])?$/.test(x);
7const base=(model:string)=>model.replace('[1m]','');
8/** Resolve a family through Claude's own --model selection, never menu order or version sorting. */
9export function parseCatalog(stdout:string,tier:Tier):Target {
10 if(stdout.length>1_000_000)throw Error('Catalog response too large');
11 const responses=stdout.split('\n').filter(Boolean).map(line=>{try{return JSON.parse(line)}catch{return null}});
12 const response=(id:string)=>{
13 const matches=responses.filter(x=>x?.type==='control_response'&&x.response?.request_id===id);
14 if(matches.length!==1||matches[0].response.subtype!=='success')throw Error('Catalog discovery failed');
15 return matches[0].response.response;
16 };
17 const models=response('jevshift-catalog')?.models;
18 const resolved=response('jevshift-resolved')?.applied?.model;
19 const family=new RegExp(`^claude-${tier}-`);
20 if(!modelId(resolved)||!family.test(resolved))throw Error('Alias did not resolve to the requested family');
21 if(!Array.isArray(models)||models.length>100)throw Error('Invalid model list');
22 const entries=models.filter(x=>modelId(x?.resolvedModel)&&family.test(x.resolvedModel));
23 const versions:Target[]=[];
24 for(const id of [...new Set<string>(entries.map(x=>x.resolvedModel))].sort()){
25 const rows=entries.filter(x=>x.resolvedModel===id);
26 // Conflicting/disabled rows are not evidence of permission or supported effort.
27 if(rows.some(x=>x.disabled===true||x.supportsEffort===false||!Array.isArray(x.supportedEffortLevels)))continue;
28 const efforts=EFFORTS.filter(e=>rows.every(x=>x.supportedEffortLevels.includes(e)));
29 if(efforts.length)versions.push({model:id,efforts});
30 }
31 if(entries.some(x=>x.resolvedModel===resolved)&&!versions.some(x=>x.model===resolved))
32 throw Error('Resolved alias entry is disabled or unsupported');
33 const matches=versions.filter(x=>base(x.model)===base(resolved));
34 const chosen=matches.find(x=>x.model===resolved)??matches.find(x=>!x.model.includes('[1m]'))??matches[0];
35 if(!chosen)throw Error('Resolved alias has no supported catalog entry');
36 // The resolver also verifies the plain ID when the menu exposes only its 1M variant.
37 if(!versions.some(x=>x.model===resolved)&&!entries.some(x=>x.resolvedModel===resolved))
38 versions.push({model:resolved,efforts:chosen.efforts});
39 return {...chosen,...(versions.length>1?{versions:versions.filter(x=>x.model!==chosen.model)}:{})};
40}
41export function contextTarget(target:Target,native:string):Target|undefined {
42 if(!native.includes('[1m]')||target.model.includes('[1m]'))return target;
43 return target.versions?.find(x=>x.model===target.model+'[1m]');
44}
45export function resolveTarget(catalog:Catalog,input:string,native=''):{tier:Tier;target:Target}|undefined {
46 for(const tier of TIERS){
47 const target=catalog[tier];if(!target)continue;
48 if(input===tier){const contextual=contextTarget(target,native);return contextual?{tier,target:contextual}:undefined;}
49 if(input===target.model)return{tier,target};
50 const exact=target.versions?.find(x=>x.model===input);if(exact)return{tier,target:exact};
51 }
52}
53export function compatible(target:Target,native:string,effort:unknown):boolean {
54 return modelId(target.model) && (effort===undefined||(typeof effort==='string'&&target.efforts.includes(effort)))
55 &&(!native.includes('[1m]')||target.model.includes('[1m]'));
56}
57export const CATALOG_ARGS=['--safe-mode','--strict-mcp-config','--mcp-config','{"mcpServers":{}}',
58 '--no-session-persistence','--input-format','stream-json','--output-format','stream-json','--verbose','-p'];
59export const CATALOG_INPUT=[['jevshift-catalog','initialize'],['jevshift-resolved','get_settings']]
60 .map(([request_id,subtype])=>JSON.stringify({type:'control_request',request_id,request:{subtype}})).join('\n')+'\n';
61src/context.ts 118 lines1/** Pure, bounded context construction. No filesystem, network, or transcript mutation. */
2export type Message = {
3 role: 'user' | 'assistant'; text: string;
4 toolUses?: readonly { tool_use_id: string; tool: string; text?: string; isError?: boolean }[];
5 toolResults?: readonly { tool_use_id: string; text: string; isError: boolean }[];
6};
7export type RequestEvidence = { text: string; origin: 'composer' | 'bridge' | 'sdk'; generation: number; instructionId?: number };
8export type ContextSnapshot = {
9 state: string | null;
10 meta: { version: 1; ready: boolean; reason: string; bytes: number; serializedBytes: number; generation: number; instructionId: number;
11 messagesIncluded: number; toolsIncluded: number; redactions: number; truncated: boolean };
12};
13export const LIMITS = Object.freeze({ bytes: 7000, serializedBytes: 7000, scanToolsPerMessage: 32, inputChars: 65536, scanMessages: 64,
14 recentMessages: 6, requestBytes: 1600, messageBytes: 350, planBytes: 1100,
15 tools: 3, toolBytes: 650 });
16
17export function bytes(s: string): number {
18 let n = 0;
19 for (const c of s) { const p = c.codePointAt(0)!; n += p <= 0x7f ? 1 : p <= 0x7ff ? 2 : p <= 0xffff ? 3 : 4; }
20 return n;
21}
22function prefix(s: string, budget: number): string {
23 let result = '', n = 0;
24 for (const c of s) { const size = bytes(c); if (n + size > budget) break; result += c; n += size; }
25 return result;
26}
27function excerpt(s: string, budget: number): string {
28 if (bytes(s) <= budget) return s;
29 const mark = '\n[excerpt clipped]\n', head = Math.floor((budget - bytes(mark)) * .72);
30 // Input is sanitized before cutting, so clipping cannot expose a secret's suffix.
31 const tail = Array.from(s).reverse().join('');
32 return prefix(s, head) + mark + Array.from(prefix(tail, budget - bytes(mark) - head)).reverse().join('');
33}
34export function redact(raw: string): { text: string; count: number; oversized: boolean } {
35 if (raw.length > LIMITS.inputChars) return { text: '[oversized content omitted]', count: 0, oversized: true };
36 let text = raw.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '');
37 let count = 0;
38 const replace = (pattern: RegExp, value: string) => { text = text.replace(pattern, () => { count++; return value; }); };
39 replace(/-----BEGIN [^-\n]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-\n]*PRIVATE KEY-----|$)/g, '[REDACTED:private_key]');
40 replace(/\b(?:sk-(?:ant-|or-)?[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{16,}|AIza[A-Za-z0-9_-]{25,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/g, '[REDACTED:credential]');
41 replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REDACTED:jwt]');
42 replace(/^(?:cookie|set-cookie)\s*:[^\n]*/gim, '[REDACTED:cookie]');
43 replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '[REDACTED:authorization]');
44 replace(/(?:["']?)(?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)|authorization|cookie)(?:["']?)\s*[:=]\s*(?:"[^"\n]*"|'[^'\n]*'|[^\s,;\n}]+)/gi, '[REDACTED:secret_assignment]');
45 replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@[^\s<>'"]+/gi, '[REDACTED:credential_url]');
46 replace(/[?&](?:key|token|secret|password|code|api_key|access_token)=[^\s&#<>"']+/gi, '[REDACTED:query_secret]');
47 replace(/[A-Za-z0-9.+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '[REDACTED:email]');
48 replace(/\/(?:Users|home)\/[^/\s<>"']+/g, '[HOME]');
49 return { text, count, oversized: false };
50}
51function xml(s: string): string { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
52const planPattern = /(?:^|\n)\s*(?:#{1,4}\s*)?(?:plan|next steps|implementation|approach|design|proposed|agreed|decision)\b|(?:^|\n)\s*(?:\d+[.)]|- \[[ x]\])\s/im;
53
54export function buildContext(messages: readonly Message[], request: RequestEvidence | null): ContextSnapshot {
55 const meta: ContextSnapshot['meta'] = { version: 1, ready: false, reason: 'missing_verified_request', bytes: 0, serializedBytes: 0,
56 generation: request?.generation ?? 0, instructionId: request?.instructionId ?? request?.generation ?? 0,
57 messagesIncluded: 0, toolsIncluded: 0, redactions: 0, truncated: false };
58 if (!request || !request.text.trim()) return { state: null, meta };
59 if (request.text.length > LIMITS.inputChars) { meta.reason = 'request_too_large'; return { state: null, meta }; }
60 const clean = (raw: string, budget: number) => {
61 const safe = redact(raw); meta.redactions += safe.count;
62 if (safe.oversized || bytes(xml(safe.text)) > budget) meta.truncated = true;
63 // Clip escaped text using a raw prefix/tail first, then reduce without breaking entities.
64 let rawBudget = budget;
65 let cut = excerpt(safe.text, rawBudget);
66 while (bytes(xml(cut)) > budget) {
67 rawBudget = Math.max(1, Math.floor(rawBudget * .65));
68 cut = rawBudget < 24 ? prefix(safe.text, rawBudget) : excerpt(safe.text, rawBudget);
69 }
70 return xml(cut);
71 };
72 const current = clean(request.text.trim(), LIMITS.requestBytes);
73 const tail = messages.slice(-LIMITS.scanMessages);
74 meta.truncated ||= messages.length > tail.length;
75 // Exact current request is already represented separately. Tool-only rows are never dialogue.
76 const currentIndex = tail.findLastIndex(m => m.role === 'user' && !m.toolResults?.length && m.text.trim() === request.text.trim());
77 const dialogue = tail.map((m, i) => ({ m, i })).filter(({ m, i }) => i !== currentIndex && m.text?.trim() && !(m.toolResults?.length));
78 const recent = dialogue.slice(-LIMITS.recentMessages);
79 meta.truncated ||= dialogue.length > recent.length;
80 const history = recent.map(({m}) => `### ${m.role === 'assistant' ? 'Assistant' : 'User-role transcript (origin not retained)'}\n\n${clean(m.text, LIMITS.messageBytes)}`);
81 meta.messagesIncluded = history.length;
82 // Quote a nearby visible plan, not an invented summary or an arbitrarily old design.
83 const candidates = dialogue.slice(-12).filter(({m}) => m.role === 'assistant');
84 const plan = candidates.slice(-3).filter(({m}) => planPattern.test(m.text)).at(-1) ?? candidates.at(-1);
85 const planText = plan ? clean(plan.m.text, LIMITS.planBytes) : '';
86 const calls = new Map<string, {tool: string; text?: string; isError?: boolean; order: number}>();
87 const results = new Map<string, {tool: string; text: string; isError: boolean; order: number}>();
88 for (let i = 0; i < tail.length; i++) {
89 const uses = tail[i].toolUses ?? [], outcomes = tail[i].toolResults ?? [];
90 meta.truncated ||= uses.length > LIMITS.scanToolsPerMessage || outcomes.length > LIMITS.scanToolsPerMessage;
91 for (const t of uses.slice(-LIMITS.scanToolsPerMessage)) {
92 calls.set(t.tool_use_id, {tool: t.tool, text: t.text, isError: t.isError, order: i});
93 if (t.text !== undefined) results.set(t.tool_use_id, {tool: t.tool, text: t.text, isError: !!t.isError, order: i});
94 }
95 for (const t of outcomes.slice(-LIMITS.scanToolsPerMessage)) results.set(t.tool_use_id,
96 {tool: calls.get(t.tool_use_id)?.tool ?? 'Tool', text: t.text, isError: t.isError, order: i});
97 }
98 const selected = [...results.values()].sort((a,b) => a.order-b.order).slice(-LIMITS.tools);
99 meta.truncated ||= results.size > selected.length;
100 const tools = selected.map(t => `### ${clean(t.tool, 80)} — ${t.isError ? 'error' : 'completed'}\n\n${clean(t.text, LIMITS.toolBytes)}`);
101 meta.toolsIncluded = tools.length;
102 const section = (name: string, text: string) => `<${name}>\n${text || '(none available)'}\n</${name}>`;
103 let state = ['# Claude Code session context', section('current_request', current),
104 section('recent_messages', history.join('\n\n')),
105 section('work_summary', planText ? 'Visible assistant plan or latest work excerpt; not independently verified.\n\n' + planText : ''),
106 section('recent_tool_results', tools.join('\n\n'))].join('\n\n');
107 // Whole-section fallback keeps XML valid and gives the current request priority.
108 if (bytes(state) > LIMITS.bytes || bytes(JSON.stringify(state)) > LIMITS.serializedBytes) {
109 meta.truncated = true; meta.messagesIncluded = 0;
110 state = ['# Claude Code session context', section('current_request', current), section('recent_messages','[omitted to fit size limit]'),
111 section('work_summary', planText), section('recent_tool_results', tools.join('\n\n'))].join('\n\n');
112 }
113 if (bytes(state) > LIMITS.bytes || bytes(JSON.stringify(state)) > LIMITS.serializedBytes) { meta.reason = 'size_limit'; return { state: null, meta }; }
114 if (bytes(JSON.stringify(state)) > LIMITS.serializedBytes) { meta.reason = 'serialized_size_limit'; return { state: null, meta }; }
115 meta.ready = true; meta.reason = 'ready'; meta.bytes = bytes(state); meta.serializedBytes = bytes(JSON.stringify(state));
116 return { state, meta };
117}
118src/jev.ts 56 lines1import { bytes } from './context.ts';
2import { JEV_PROMPT } from './jev-prompt.ts';
3import {isEffort,type Effort} from './effort.ts';
4import type {Catalog} from './auto.ts';
5
6export const ENDPOINT = 'https://openrouter.ai/api/alpha/decisions';
7export type Tier = 'sonnet' | 'opus' | 'fable';
8export type Usage = {inputTokens: number; outputTokens: number; costUsd: number};
9export type Reply = {status: number; ok: boolean; text: string};
10export type Decision = {choice: Tier; effort?:Effort; confidence?: number; probabilities?: Record<Tier,number>; model: string};
11export type DecisionScope = {catalog?:Catalog; pinnedModel?:Tier; fixedEffort?:Effort|'native'; nativeEffort?:unknown};
12export type Parsed = {outcome: 'recommended' | 'http_error' | 'invalid_response'; decision?: Decision; usage?: Usage; httpStatus: number};
13const object = (x: unknown): x is Record<string,any> => !!x && typeof x === 'object' && !Array.isArray(x);
14const probability = (x: unknown): x is number => typeof x === 'number' && Number.isFinite(x) && x >= 0 && x <= 1;
15const count = (x: unknown): x is number => Number.isSafeInteger(x) && (x as number) >= 0;
16export function requestBody(state: string,scope:DecisionScope={}): string | null {
17 if(scope.pinnedModel&&scope.fixedEffort)return null;
18 const questions={...(!scope.pinnedModel?{model_choice:JEV_PROMPT.questions.model_choice}:{}),
19 ...(!scope.fixedEffort?{effort_choice:JEV_PROMPT.questions.effort_choice}:{})};
20 const available=scope.catalog?Object.entries(scope.catalog).map(([tier,target])=>
21 `${tier}: ${target!.model}; allowed effort: ${target!.efforts.join(', ')}`).join('\n'):'';
22 const constraints=[available,scope.pinnedModel?`Model pinned to ${scope.pinnedModel}; choose only its effort.`:'',
23 scope.fixedEffort?`Effort fixed to ${scope.fixedEffort==='native'?String(scope.nativeEffort??'model default'):scope.fixedEffort}; choose only a compatible model.`:''].filter(Boolean).join('\n');
24 const make=()=>JSON.stringify({model:JEV_PROMPT.model,questions,state:constraints?`<available_models>\n${constraints}\n</available_models>\n\n${state}`:state});
25 let body=make();
26 // Retain the verified current request. Remove older evidence before exceeding the total cap.
27 for(const section of ['recent_messages','work_summary','recent_tool_results']){
28 if(bytes(body)<=9000)break;
29 state=state.replace(new RegExp(`<${section}>[\\s\\S]*?</${section}>`),`<${section}>\n[omitted to fit request limit]\n</${section}>`);
30 body=make();
31 }
32 return bytes(body)<=9000?body:null;
33}
34/** Never return raw response bodies or server error text to diagnostics. */
35export function parseReply(reply: Reply,scope:DecisionScope={}): Parsed {
36 const base: Parsed = {outcome: reply.ok ? 'invalid_response' : 'http_error', httpStatus:reply.status};
37 if (reply.text.length > 65536) return base;
38 let x: any;
39 try { x = JSON.parse(reply.text); } catch { return base; }
40 if (!object(x)) return base;
41 const u = x.usage;
42 if (object(u) && count(u.input_tokens) && count(u.output_tokens) && typeof u.cost === 'number' && Number.isFinite(u.cost) && u.cost >= 0)
43 base.usage = {inputTokens:u.input_tokens, outputTokens:u.output_tokens, costUsd:u.cost};
44 if (!reply.ok) return base;
45 const validChoice=(a:any,labels:string[])=>object(a)&&a.type==='choice'&&labels.includes(a.choice)&&probability(a.confidence)&&
46 object(a.probabilities)&&Object.keys(a.probabilities).sort().join(',')===labels.slice().sort().join(',')&&
47 Object.values(a.probabilities).every(probability)&&Math.abs(Object.values(a.probabilities).reduce((sum:number,p:any)=>sum+p,0)-1)<.02;
48 const a=x.answers?.model_choice,e=x.answers?.effort_choice;
49 if((!scope.pinnedModel&&!validChoice(a,['sonnet','opus','fable'])) ||
50 (!scope.fixedEffort&&!validChoice(e,['low','medium','high','xhigh','max'])) ||
51 typeof x.model!=='string'||!/^typesafe\/jev-1\.13(?:-\d{8})?$/.test(x.model))return base;
52 const effort=scope.fixedEffort==='native'?(isEffort(scope.nativeEffort)?scope.nativeEffort:undefined):scope.fixedEffort??e.choice;
53 return {...base,outcome:'recommended',decision:{choice:scope.pinnedModel??a.choice,effort,
54 ...(!scope.pinnedModel?{confidence:a.confidence,probabilities:a.probabilities}:{}),model:x.model}};
55}
56src/transport.ts 36 lines1import type {ProcessRunInit,ProcessRunResult} from 'claude-code';
2import {ENDPOINT,type Reply} from './jev.ts';
3import {bytes} from './context.ts';
4import {EVALUATOR_LIMITS} from './evaluator-limits.ts';
5export const TRANSPORT_LIMITS=EVALUATOR_LIMITS;
6export type TransportOutcome='transport_timeout'|'transport_unavailable'|'response_too_large'|'invalid_transport'|'transport_disabled';
7export class TransportError extends Error {
8 constructor(readonly outcome:TransportOutcome){super(outcome)}
9}
10export type RunProcess=(argv:readonly string[],init:ProcessRunInit)=>Promise<ProcessRunResult>;
11type Init={method?:string;headers?:Record<string,string>;body?:string};
12// Curl config quoting, NOT shell quoting. Nothing passes through a shell.
13const quote=(s:string)=>'"'+s.replace(/\\/g,'\\\\').replace(/"/g,'\\"').replace(/\n/g,'\\n').replace(/\r/g,'\\r').replace(/\t/g,'\\t').replace(/\v/g,'\\v')+'"';
14export async function boundedFetch(run:RunProcess,url:string,init:Init):Promise<Reply>{
15 const auth=init.headers?.Authorization;
16 if(url!==ENDPOINT||init.method!=='POST'||typeof init.body!=='string'||bytes(init.body)>9000||
17 typeof auth!=='string'||!auth.startsWith('Bearer ')||auth.length>4103||/[\x00-\x1f\x7f]/.test(auth)||init.body.includes('\0'))throw new TransportError('invalid_transport');
18 const config=[`url = ${quote(ENDPOINT)}`,'request = "POST"',`header = ${quote(auth?'Authorization: '+auth:'')}`,
19 'header = "Content-Type: application/json"',`data-raw = ${quote(init.body)}`].join('\n')+'\n';
20 let result:ProcessRunResult;
21 try{
22 result=await run(['curl','--disable','--silent','--show-error','--globoff','--proto','=https','--proto-redir','=https',
23 '--max-redirs','0','--retry','0','--connect-timeout',String(TRANSPORT_LIMITS.connectSeconds),'--max-time',String(TRANSPORT_LIMITS.transferSeconds),
24 '--max-filesize',String(TRANSPORT_LIMITS.responseBytes),'--write-out','\n%{http_code}','--config','-'],
25 {stdin:config,timeoutMs:TRANSPORT_LIMITS.processMs,env:{OPENROUTER_API_KEY:''}});
26 }catch{throw new TransportError('transport_unavailable')}
27 // Never pass stderr or partial response bodies into diagnostics.
28 if(result.exitCode===28)throw new TransportError('transport_timeout');
29 if(result.exitCode===63||bytes(result.stdout)>TRANSPORT_LIMITS.responseBytes+4)throw new TransportError('response_too_large');
30 if(result.exitCode!==0)throw new TransportError('transport_unavailable');
31 const match=/\n([1-5]\d\d)$/.exec(result.stdout);
32 if(!match)throw new TransportError('invalid_transport');
33 const status=Number(match[1]);
34 return {status,ok:status>=200&&status<300,text:result.stdout.slice(0,-4)};
35}
36