@@ -85,7 +85,8 @@ const VLM_URL = process.env.AEGIS_VLM_URL || getArg('vlm', '');
8585const RESULTS_DIR = getArg ( 'out' , path . join ( os . homedir ( ) , '.aegis-ai' , 'benchmarks' ) ) ;
8686const IS_SKILL_MODE = ! ! process . env . AEGIS_SKILL_ID ;
8787const NO_OPEN = args . includes ( '--no-open' ) || skillParams . noOpen || false ;
88- const TEST_MODE = skillParams . mode || 'full' ;
88+ // Auto-detect mode: if no VLM URL, default to 'llm' (skip VLM image-analysis tests)
89+ const TEST_MODE = skillParams . mode || ( VLM_URL ? 'full' : 'llm' ) ;
8990const IDLE_TIMEOUT_MS = 30000 ; // Streaming idle timeout — resets on each received token
9091const FIXTURES_DIR = path . join ( __dirname , '..' , 'fixtures' ) ;
9192
@@ -190,18 +191,42 @@ async function llmCall(messages, opts = {}) {
190191
191192 // Sanitize messages for llama-server compatibility:
192193 // - Replace null content with empty string (llama-server rejects null)
193- messages = messages . map ( m => ( {
194- ...m ,
195- ...( m . content === null && { content : '' } ) ,
196- } ) ) ;
194+ // - Convert tool_calls assistant messages to plain text (llama-server
195+ // doesn't support OpenAI tool_calls format in conversation history)
196+ // - Convert tool result messages to user messages
197+ messages = messages . map ( m => {
198+ if ( m . role === 'assistant' && m . tool_calls ) {
199+ // Convert tool call to text representation
200+ const callDesc = m . tool_calls . map ( tc =>
201+ `[Calling ${ tc . function . name } (${ tc . function . arguments } )]`
202+ ) . join ( '\n' ) ;
203+ return { role : 'assistant' , content : callDesc } ;
204+ }
205+ if ( m . role === 'tool' ) {
206+ // Convert tool result to user message
207+ return { role : 'user' , content : `[Tool result]: ${ m . content } ` } ;
208+ }
209+ return {
210+ ...m ,
211+ ...( m . content === null && { content : '' } ) ,
212+ } ;
213+ } ) ;
214+
215+ // Determine the correct max-tokens parameter name:
216+ // - OpenAI cloud (GPT-5.4+): requires 'max_completion_tokens', rejects 'max_tokens'
217+ // - Local llama-server: requires 'max_tokens', may not understand 'max_completion_tokens'
218+ const isCloudApi = ! opts . vlm && ( LLM_API_TYPE === 'openai' || LLM_BASE_URL . includes ( 'openai.com' ) || LLM_BASE_URL . includes ( 'api.anthropic' ) ) ;
219+ const maxTokensParam = opts . maxTokens
220+ ? ( isCloudApi ? { max_completion_tokens : opts . maxTokens } : { max_tokens : opts . maxTokens } )
221+ : { } ;
197222
198223 // Build request params
199224 const params = {
200225 messages,
201226 stream : true ,
202227 ...( model && { model } ) ,
203228 ...( opts . temperature !== undefined && { temperature : opts . temperature } ) ,
204- ...( opts . maxTokens && { max_tokens : opts . maxTokens } ) ,
229+ ...maxTokensParam ,
205230 ...( opts . expectJSON && opts . temperature === undefined && { temperature : 0.7 } ) ,
206231 ...( opts . expectJSON && { top_p : 0.8 } ) ,
207232 ...( opts . tools && { tools : opts . tools } ) ,
@@ -353,7 +378,12 @@ async function llmCall(messages, opts = {}) {
353378}
354379
355380function stripThink ( text ) {
356- return text . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > \s * / gi, '' ) . trim ( ) ;
381+ // Strip standard <think>...</think> tags
382+ let cleaned = text . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > \s * / gi, '' ) . trim ( ) ;
383+ // Strip Qwen3.5 'Thinking Process:' blocks (outputs plain text reasoning
384+ // instead of <think> tags when enable_thinking is active)
385+ cleaned = cleaned . replace ( / ^ T h i n k i n g P r o c e s s [: \s] * [ \s \S ] * ?(? = \n \s * [ { \[ ] | \n ` ` ` | $ ) / i, '' ) . trim ( ) ;
386+ return cleaned ;
357387}
358388
359389function parseJSON ( text ) {
@@ -364,7 +394,7 @@ function parseJSON(text) {
364394 jsonStr = codeBlock [ 1 ] ;
365395 } else {
366396 // Find first { or [ and extract balanced JSON
367- const startIdx = cleaned . search ( / [ { [ ] / ) ;
397+ const startIdx = cleaned . search ( / [ { \ [] / ) ;
368398 if ( startIdx >= 0 ) {
369399 const opener = cleaned [ startIdx ] ;
370400 const closer = opener === '{' ? '}' : ']' ;
@@ -383,6 +413,15 @@ function parseJSON(text) {
383413 }
384414 }
385415 }
416+ // Clean common local model artifacts before parsing:
417+ // - Replace literal "..." or "…" placeholders in arrays/values
418+ // - Replace <indices> placeholder tags
419+ jsonStr = jsonStr
420+ . replace ( / , \s * \. { 3 , } \s * (? = [ \] } , ] ) / g, '' ) // trailing ..., before ] } or ,
421+ . replace ( / \. { 3 , } / g, '"..."' ) // standalone ... → string
422+ . replace ( / … / g, '"..."' ) // ellipsis char
423+ . replace ( / < [ a - z _ ] + > / gi, '"placeholder"' ) // <indices> etc.
424+ . replace ( / , \s * ( [ } \] ] ) / g, '$1' ) ; // trailing commas
386425 return JSON . parse ( jsonStr . trim ( ) ) ;
387426}
388427
@@ -2090,7 +2129,10 @@ async function main() {
20902129 } ) ;
20912130
20922131 log ( '' ) ;
2093- process . exit ( failed > 0 ? 1 : 0 ) ;
2132+ // When running as Aegis skill, always exit 0 — test results are reported
2133+ // via JSON events (pass/fail is a result, not an error). Exit 1 only for
2134+ // standalone CLI usage where CI/CD pipelines expect non-zero on failures.
2135+ process . exit ( IS_SKILL_MODE ? 0 : ( failed > 0 ? 1 : 0 ) ) ;
20942136}
20952137
20962138// Run when executed directly — supports both plain Node and Electron spawn.
0 commit comments