@@ -2,8 +2,8 @@ import { Provider } from "@/provider/provider"
22import * as Log from "@opencode-ai/core/util/log"
33import { Context , Effect , Layer , Record } from "effect"
44import * as Stream from "effect/Stream"
5- import { streamText , wrapLanguageModel , type ModelMessage , type Tool , tool , jsonSchema } from "ai"
6- import type { LLMEvent } from "@opencode-ai/llm"
5+ import { streamText , wrapLanguageModel , type ModelMessage , type Tool , tool as aiTool , jsonSchema , asSchema } from "ai"
6+ import { tool as nativeTool , ToolFailure , type JsonSchema , type LLMEvent } from "@opencode-ai/llm"
77import { LLMClient , RequestExecutor } from "@opencode-ai/llm/route"
88import { mergeDeep } from "remeda"
99import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
@@ -18,6 +18,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
1818import { Permission } from "@/permission"
1919import { PermissionID } from "@/permission/schema"
2020import { Bus } from "@/bus"
21+ import { errorMessage } from "@/util/error"
2122import { Wildcard } from "@/util/wildcard"
2223import { SessionID } from "@/session/schema"
2324import { Auth } from "@/auth"
@@ -216,7 +217,7 @@ const live: Layer.Layer<
216217 Object . keys ( tools ) . length === 0 &&
217218 hasToolCalls ( input . messages )
218219 ) {
219- tools [ "_noop" ] = tool ( {
220+ tools [ "_noop" ] = aiTool ( {
220221 description : "Do not call this tool. It exists only for API compatibility and must never be invoked." ,
221222 inputSchema : jsonSchema ( {
222223 type : "object" ,
@@ -358,31 +359,31 @@ const live: Layer.Layer<
358359 if ( input . model . providerID !== "openai" || input . model . api . npm !== "@ai-sdk/openai" ) {
359360 return yield * Effect . fail ( new Error ( "Native LLM runtime currently only supports OpenAI models" ) )
360361 }
361- if ( Object . keys ( sortedTools ) . length > 0 ) {
362- return yield * Effect . fail ( new Error ( "Native LLM runtime does not support tools yet" ) )
363- }
364362 const apiKey =
365363 info ?. type === "api" ? info . key : typeof item . options . apiKey === "string" ? item . options . apiKey : undefined
366364 if ( ! apiKey ) return yield * Effect . fail ( new Error ( "Native LLM runtime requires API key auth for OpenAI" ) )
367365 const baseURL = typeof item . options . baseURL === "string" ? item . options . baseURL : undefined
366+ const request = LLMNative . request ( {
367+ model : input . model ,
368+ apiKey,
369+ baseURL,
370+ system : isOpenaiOauth ? system : [ ] ,
371+ messages : ProviderTransform . message ( messages , input . model , options ) ,
372+ tools : sortedTools ,
373+ toolChoice : input . toolChoice ,
374+ temperature : params . temperature ,
375+ topP : params . topP ,
376+ topK : params . topK ,
377+ maxOutputTokens : params . maxOutputTokens ,
378+ providerOptions : ProviderTransform . providerOptions ( input . model , params . options ) ,
379+ headers : requestHeaders ,
380+ } )
368381 return {
369382 type : "native" as const ,
370- stream : LLMClient . stream (
371- LLMNative . request ( {
372- model : input . model ,
373- apiKey,
374- baseURL,
375- system : isOpenaiOauth ? system : [ ] ,
376- messages : ProviderTransform . message ( messages , input . model , options ) ,
377- toolChoice : input . toolChoice ,
378- temperature : params . temperature ,
379- topP : params . topP ,
380- topK : params . topK ,
381- maxOutputTokens : params . maxOutputTokens ,
382- providerOptions : ProviderTransform . providerOptions ( input . model , params . options ) ,
383- headers : requestHeaders ,
384- } ) ,
385- ) . pipe ( Stream . provide ( LLMClient . layer ) , Stream . provide ( RequestExecutor . defaultLayer ) ) ,
383+ stream : LLMClient . stream ( { request, tools : nativeTools ( sortedTools , input ) } ) . pipe (
384+ Stream . provide ( LLMClient . layer ) ,
385+ Stream . provide ( RequestExecutor . defaultLayer ) ,
386+ ) ,
386387 }
387388 }
388389
@@ -502,6 +503,37 @@ function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission"
502503 return Record . filter ( input . tools , ( _ , k ) => input . user . tools ?. [ k ] !== false && ! disabled . has ( k ) )
503504}
504505
506+ function nativeSchema ( value : unknown ) : JsonSchema {
507+ if ( ! value || typeof value !== "object" ) return { type : "object" , properties : { } }
508+ if ( "jsonSchema" in value && value . jsonSchema && typeof value . jsonSchema === "object" )
509+ return value . jsonSchema as JsonSchema
510+ return asSchema ( value as Parameters < typeof asSchema > [ 0 ] ) . jsonSchema as JsonSchema
511+ }
512+
513+ function nativeTools ( tools : Record < string , Tool > , input : StreamRequest ) {
514+ return Object . fromEntries (
515+ Object . entries ( tools ) . map ( ( [ name , item ] ) => [
516+ name ,
517+ nativeTool ( {
518+ description : item . description ?? "" ,
519+ jsonSchema : nativeSchema ( item . inputSchema ) ,
520+ execute : ( args : unknown , ctx ?: { readonly id : string ; readonly name : string } ) =>
521+ Effect . tryPromise ( {
522+ try : ( ) => {
523+ if ( ! item . execute ) throw new Error ( `Tool has no execute handler: ${ name } ` )
524+ return item . execute ( args , {
525+ toolCallId : ctx ?. id ?? name ,
526+ messages : input . messages ,
527+ abortSignal : input . abort ,
528+ } )
529+ } ,
530+ catch : ( error ) => new ToolFailure ( { message : errorMessage ( error ) } ) ,
531+ } ) ,
532+ } ) ,
533+ ] ) ,
534+ )
535+ }
536+
505537// Check if messages contain any tool-call content
506538// Used to determine if a dummy tool should be added for LiteLLM proxy compatibility
507539export function hasToolCalls ( messages : ModelMessage [ ] ) : boolean {
0 commit comments