feat(llm): pass strict through tool definitions for Codex parity
This commit is contained in:
@@ -258,6 +258,7 @@ const lowerTool = (tool: ToolDefinition): OpenAIResponsesTool => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: ProviderShared.openAiToolInputSchema(tool.inputSchema),
|
||||
...(tool.strict != null ? { strict: tool.strict } : {}),
|
||||
})
|
||||
|
||||
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
|
||||
@@ -241,6 +241,7 @@ export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefini
|
||||
description: Schema.String,
|
||||
inputSchema: JsonSchema,
|
||||
outputSchema: Schema.optional(JsonSchema),
|
||||
strict: Schema.optional(Schema.Boolean),
|
||||
cache: Schema.optional(CacheHint),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
|
||||
@@ -88,6 +88,7 @@ type TypedToolConfig = {
|
||||
readonly execute?: ToolExecute<ToolSchema<any>, ToolSchema<any>>
|
||||
readonly toModelOutput?: ToolToModelOutput<ToolSchema<any>, ToolSchema<any>>
|
||||
readonly toStructuredOutput?: (output: unknown) => unknown
|
||||
readonly strict?: boolean
|
||||
}
|
||||
|
||||
type DynamicToolConfig = {
|
||||
@@ -97,6 +98,7 @@ type DynamicToolConfig = {
|
||||
readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
|
||||
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
|
||||
readonly toStructuredOutput?: (output: unknown) => unknown
|
||||
readonly strict?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +183,7 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
|
||||
description: config.description,
|
||||
inputSchema: config.jsonSchema,
|
||||
outputSchema: config.outputSchema,
|
||||
strict: config.strict,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -201,6 +204,7 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
|
||||
description: config.description,
|
||||
inputSchema: toJsonSchema(config.parameters),
|
||||
outputSchema: toJsonSchema(config.success),
|
||||
strict: config.strict,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -226,6 +230,7 @@ export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass>
|
||||
description: item._definition.description,
|
||||
inputSchema: item._definition.inputSchema,
|
||||
outputSchema: item._definition.outputSchema,
|
||||
strict: item._definition.strict,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -166,7 +166,11 @@ function nativeSchema(value: unknown): JsonSchema {
|
||||
return asSchema(value as Parameters<typeof asSchema>[0]).jsonSchema as JsonSchema
|
||||
}
|
||||
|
||||
export function nativeTools(tools: Record<string, Tool>, input: Pick<StreamInput, "messages" | "abort">) {
|
||||
export function nativeTools(
|
||||
tools: Record<string, Tool>,
|
||||
input: Pick<StreamInput, "messages" | "abort" | "model">,
|
||||
) {
|
||||
const strict = input.model.api.npm === "@ai-sdk/openai" ? false : undefined
|
||||
return Object.fromEntries(
|
||||
Object.entries(tools).map(([name, item]) => [
|
||||
name,
|
||||
@@ -175,6 +179,7 @@ export function nativeTools(tools: Record<string, Tool>, input: Pick<StreamInput
|
||||
NativeTool.make({
|
||||
description: item.description ?? "",
|
||||
jsonSchema: nativeSchema(item.inputSchema),
|
||||
...(strict !== undefined && { strict }),
|
||||
execute: (args: unknown, ctx) =>
|
||||
Effect.tryPromise({
|
||||
try: () => {
|
||||
|
||||
@@ -146,6 +146,16 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
|
||||
)
|
||||
|
||||
const tools = resolveTools(input)
|
||||
// Codex parity: OpenAI Responses-family providers hardcode `strict: false`
|
||||
// on every function tool so MCP-sourced and dynamic schemas that don't
|
||||
// satisfy OpenAI's structured-outputs constraints still register.
|
||||
if (
|
||||
input.model.api.npm === "@ai-sdk/openai" ||
|
||||
input.model.api.npm === "@ai-sdk/azure" ||
|
||||
input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
|
||||
) {
|
||||
for (const key of Object.keys(tools)) tools[key] = { ...tools[key], strict: false }
|
||||
}
|
||||
if (
|
||||
input.model.providerID.includes("github-copilot") &&
|
||||
Object.keys(tools).length === 0 &&
|
||||
|
||||
@@ -511,7 +511,7 @@ describe("session.llm-native.request", () => {
|
||||
},
|
||||
} satisfies Tool,
|
||||
},
|
||||
{ messages: [] as ModelMessage[], abort: new AbortController().signal },
|
||||
{ messages: [] as ModelMessage[], abort: new AbortController().signal, model: baseModel },
|
||||
)
|
||||
|
||||
const failure = yield* Effect.flip(wrapped.explode.execute({}, { id: "call-1", name: "explode" }))
|
||||
@@ -527,7 +527,7 @@ describe("session.llm-native.request", () => {
|
||||
// wiring is wrong; we want a typed failure, not a silent skip or unhandled exception.
|
||||
const wrapped = LLMNativeRuntime.nativeTools(
|
||||
{ incomplete: { description: "no execute", inputSchema: jsonSchema({ type: "object" }) } satisfies Tool },
|
||||
{ messages: [] as ModelMessage[], abort: new AbortController().signal },
|
||||
{ messages: [] as ModelMessage[], abort: new AbortController().signal, model: baseModel },
|
||||
)
|
||||
|
||||
const failure = yield* Effect.flip(wrapped.incomplete.execute({}, { id: "call-1", name: "incomplete" }))
|
||||
|
||||
Reference in New Issue
Block a user