@@ -3,7 +3,9 @@ const debug = debugModule('codeceptjs:ai')
33import output from './output.js'
44import event from './event.js'
55import { removeNonInteractiveElements , minifyHtml , splitByChunks } from './html.js'
6- import { generateText } from 'ai'
6+ import { generateText , experimental_decide as decide , APICallError } from 'ai'
7+ import { createOpenRouter } from '@openrouter/ai-sdk-provider'
8+ import { createTypeSafeAi } from '@ai-sdk/typesafe-ai'
79import { fileURLToPath } from 'url'
810import path from 'path'
911import { fileExists , resolveImportModulePath } from './utils.js'
@@ -271,9 +273,9 @@ function parseCodeBlocks(response) {
271273 return modifiedSnippets . filter ( snippet => ! ! snippet )
272274}
273275
274- const DECISION_ENDPOINTS = {
275- openrouter : { url : 'https://openrouter.ai/api/alpha/decisions' , keyName : 'OPENROUTER_API_KEY' } ,
276- typesafe : { url : 'https://api.typesafe.ai/v1/systemone' , keyName : 'TYPESAFE_API_KEY' } ,
276+ const DECISION_PROVIDERS = {
277+ openrouter : { keyName : 'OPENROUTER_API_KEY' } ,
278+ typesafe : { keyName : 'TYPESAFE_API_KEY' } ,
277279}
278280
279281const defaultDecisionConfig = {
@@ -292,20 +294,20 @@ class DecisionAI {
292294 this . config = { ...defaultDecisionConfig , ...config }
293295
294296 const { provider, confidence } = this . config
295- this . endpoint = DECISION_ENDPOINTS [ provider ]
296- if ( ! this . endpoint ) throw new Error ( `Unknown decision provider "${ provider } " in ai.decisionModel, use one of: ${ Object . keys ( DECISION_ENDPOINTS ) . join ( ', ' ) } ` )
297+ this . provider = DECISION_PROVIDERS [ provider ]
298+ if ( ! this . provider ) throw new Error ( `Unknown decision provider "${ provider } " in ai.decisionModel, use one of: ${ Object . keys ( DECISION_PROVIDERS ) . join ( ', ' ) } ` )
297299 if ( ! ( confidence > 0 && confidence < 1 ) ) throw new Error ( `ai.decisionModel.confidence must be between 0 and 1, got ${ confidence } ` )
298300
299301 this . fetchImpl = fetch
300302 }
301303
302304 checkModel ( ) {
303- if ( this . config . apiKey || process . env [ this . endpoint . keyName ] ) return
305+ if ( this . config . apiKey || process . env [ this . provider . keyName ] ) return
304306
305307 const noKeyErrorMessage = `
306308 No API key is set for decision model.
307309
308- [!] Set ${ this . endpoint . keyName } environment variable or apiKey in ai.decisionModel config.
310+ [!] Set ${ this . provider . keyName } environment variable or apiKey in ai.decisionModel config.
309311
310312 Example (connect to OpenRouter, default):
311313
@@ -340,48 +342,48 @@ class DecisionAI {
340342 async decide ( model , state , statements ) {
341343 this . checkModel ( )
342344
343- const apiKey = this . config . apiKey || process . env [ this . endpoint . keyName ]
344- const questions = Object . fromEntries ( statements . map ( ( statement , i ) => [ `q${ i } ` , { type : 'noul' , instructions : statement } ] ) )
345+ const questions = Object . fromEntries ( statements . map ( ( statement , i ) => [ `q${ i } ` , { type : 'boolean' , instructions : statement } ] ) )
345346 debug ( 'Decision request' , model , statements )
346347
347348 const controller = new AbortController ( )
348349 const timer = setTimeout ( ( ) => controller . abort ( ) , this . config . timeout )
349350
350351 let result
351352 try {
352- let response
353- try {
354- response = await this . fetchImpl ( this . endpoint . url , {
355- method : 'POST' ,
356- headers : { Authorization : `Bearer ${ apiKey } ` , 'Content-Type' : 'application/json' } ,
357- body : JSON . stringify ( { model, state, questions } ) ,
358- signal : controller . signal ,
359- } )
360- } catch ( err ) {
361- throw new DecisionConnectionError ( `Decision model ${ model } request failed: ${ err . message } ` )
362- }
363-
364- if ( ! response . ok ) {
365- const body = await response . text ( ) . catch ( ( ) => '' )
366- throw new Error ( `Decision model ${ model } responded with ${ response . status } : ${ body } ` )
367- }
368-
369- result = await response . json ( )
353+ result = await decide ( {
354+ model : this . decisionModel ( model ) ,
355+ state,
356+ questions,
357+ maxRetries : 0 ,
358+ abortSignal : controller . signal ,
359+ } )
370360 } catch ( err ) {
371361 if ( controller . signal . aborted ) throw new DecisionConnectionError ( `Decision model ${ model } did not respond in ${ this . config . timeout } ms` )
362+ if ( APICallError . isInstance ( err ) ) throw new Error ( `Decision model ${ model } responded with ${ err . statusCode } : ${ err . responseBody || err . message } ` )
372363 throw err
373364 } finally {
374365 clearTimeout ( timer )
375366 }
376367
377- debug ( 'Decision response' , result ? .answers , result ? .usage )
368+ debug ( 'Decision response' , result . answers , result . usage )
378369
379- const answers = result ?. answers || { }
380- return statements . map ( ( statement , i ) => {
381- const probability = answers [ `q${ i } ` ] ?. noul
382- if ( typeof probability !== 'number' ) throw new Error ( `Decision model ${ model } returned no answer for "${ statement } "` )
383- return probability
384- } )
370+ return Object . keys ( questions ) . map ( id => result . answers [ id ] . probability )
371+ }
372+
373+ decisionModel ( model ) {
374+ const settings = {
375+ apiKey : this . config . apiKey || process . env [ this . provider . keyName ] ,
376+ fetch : async ( url , options ) => {
377+ try {
378+ return await this . fetchImpl ( url , options )
379+ } catch ( err ) {
380+ if ( options . signal ?. aborted ) throw err
381+ throw new DecisionConnectionError ( `Decision model ${ model } request failed: ${ err . message } ` )
382+ }
383+ } ,
384+ }
385+ if ( this . config . provider === 'typesafe' ) return createTypeSafeAi ( settings ) . decisionModel ( model )
386+ return createOpenRouter ( settings ) . evaluationModel ( model )
385387 }
386388}
387389
0 commit comments