App gateway: an unknown model name on /v1/messages uses the one loaded model (Claude Desktop sends claude-5-5-sonnet); chat and Responses stay exact, and with several models loaded the 400 stays (#5312)

This commit is contained in:
Niko1221 committed 2026-11-06 19:55:58 +01:00
1 parent c49f198c77
commit ea765a5717
2 files changed
+18 -1

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
/** Preserve native protocol events and tool payloads; the gateway owns identity and cancellation. */
import { once } from 'node:events';
export async function nativeGateway({req,res,path,body,control,serving,fetcher=fetch}){
let instance;try{instance=control.resolveInstance(body.model);}catch(e){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}
let instance;try{instance=control.resolveInstance(body.model);}catch(e){/* #5312: an Anthropic client sends a model name of its own (Claude Desktop: claude-5-5-sonnet) and cannot change it: with exactly one model loaded the route answers with it */if(path==='/v1/messages'&&body.model){try{instance=control.resolveInstance(undefined);}catch{}}if(!instance){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}}
const abort=new AbortController(),started=Date.now();let sawDone=false;/* #4690: a client that hangs up after `data: [DONE]` (the OpenAI SDK does) has the whole answer: that is not a cancel. */res.on('close',()=>{if(!res.writableEnded&&!sawDone)abort.abort();});
const entry=serving.open({client:req.headers['user-agent']?.slice(0,100)??'API client',endpoint:path.slice(3),model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>abort.abort()});
let tokensIn=null,tokensOut=null,tps=null,promptTps=null;
+17
View File
@@ -62,3 +62,20 @@ test('an Anthropic stream: the prompt count (message_start) and the output count
assert.equal(records[0].tokensIn,29757);assert.equal(records[0].tokensOut,561);assert.equal(records[0].usageSource,'reported');
}finally{await close(gateway);await close(engine);}
});
test('an Anthropic client that sends a model name of its own gets the one loaded model (#5312); OpenAI and Responses stay exact',{timeout:10000},async()=>{
const received=[];const engine=http.createServer(async(req,res)=>{let text='';for await(const chunk of req)text+=chunk;received.push(JSON.parse(text));res.writeHead(200,{'content-type':'application/json'});res.end(JSON.stringify({type:'message',content:[{type:'text',text:'Hi'}],usage:{input_tokens:1,output_tokens:1}}));});
const port=await listen(engine);const records=[];
const serving={open:value=>{const r={...value,id:String(records.length)};records.push(r);return r;},mark:(id,value)=>Object.assign(records[Number(id)],value),finish:(id,value)=>Object.assign(records[Number(id)],value)};
const one={id:'only',apiModelId:'iq3_s',port,key:'k',gpus:[0]},two={id:'other',apiModelId:'iq2_xs',port,key:'k',gpus:[1]};
const make=instances=>({resolveInstance:id=>{const found=instances.find(i=>i.apiModelId===id)??(!id&&instances.length===1?instances[0]:undefined);if(!found)throw new Error('Choose a loaded model ID.');return found;}});
const serve=control=>http.createServer(async(req,res)=>{let text='';for await(const chunk of req)text+=chunk;await nativeGateway({req,res,path:req.url,body:JSON.parse(text),control,serving});});
const single=serve(make([one])),pair=serve(make([one,two]));const p1=await listen(single),p2=await listen(pair);
const call=(p,path,body)=>fetch(`http://127.0.0.1:${p}${path}`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});
try{
let r=await call(p1,'/v1/messages',{model:'claude-5-5-sonnet',messages:[{role:'user',content:'Hi'}],max_tokens:5});assert.equal(r.status,200);await r.text();assert.equal(received.at(-1).model,'iq3_s');
r=await call(p2,'/v1/messages',{model:'claude-5-5-sonnet',messages:[],max_tokens:5});assert.equal(r.status,400);assert.equal((await r.json()).error.param,'model');
r=await call(p1,'/v1/chat/completions',{model:'claude-5-5-sonnet',messages:[]});assert.equal(r.status,400);
r=await call(p1,'/v1/responses',{model:'claude-5-5-sonnet',input:'Hi'});assert.equal(r.status,400);
}finally{await close(single);await close(pair);await close(engine);}
});