diff --git a/apps/gateway/src/lib/get-tool-args-from-request.ts b/apps/gateway/src/lib/get-tool-args-from-request.ts index e59e792d..781c8086 100644 --- a/apps/gateway/src/lib/get-tool-args-from-request.ts +++ b/apps/gateway/src/lib/get-tool-args-from-request.ts @@ -1,5 +1,5 @@ import type { AdminDeployment, Tool } from '@agentic/platform-types' -import { assert } from '@agentic/platform-core' +import { assert, HttpError } from '@agentic/platform-core' import type { GatewayHonoContext } from './types' import { cfValidateJsonSchema } from './cf-validate-json-schema' @@ -14,6 +14,7 @@ export async function getToolArgsFromRequest( deployment: AdminDeployment } ): Promise> { + const logger = ctx.get('logger') const request = ctx.req.raw assert( deployment.origin.type !== 'raw', @@ -50,17 +51,29 @@ export async function getToolArgsFromRequest( try { incomingRequestArgsRaw = (await request.json()) as Record - } catch { - // If the request body is not JSON or malformed, ignore it for now. - // TODO: need to improve on this logic. + } catch (err) { + // Error if the request body is not JSON or is malformed. + logger.error('Error parsing incoming request body', request, err) + throw new HttpError({ + message: 'Invalid request body json', + statusCode: 400, + cause: err + }) } + // console.log( + // 'incomingRequestArgsRaw', + // typeof incomingRequestArgsRaw, + // request.headers, + // incomingRequestArgsRaw + // ) + // TODO: Proper support for empty params with POST requests assert(incomingRequestArgsRaw, 400, 'Invalid empty request body') assert( typeof incomingRequestArgsRaw === 'object', 400, - 'Invalid request body' + `Invalid request body: expected type "object", received type "${typeof incomingRequestArgsRaw}"` ) assert(!Array.isArray(incomingRequestArgsRaw), 400, 'Invalid request body') return incomingRequestArgsRaw diff --git a/examples/ts-sdks/openai/bin/weather-responses.ts b/examples/ts-sdks/openai/bin/weather-responses.ts index e48accc3..af1ec2e4 100644 --- a/examples/ts-sdks/openai/bin/weather-responses.ts +++ b/examples/ts-sdks/openai/bin/weather-responses.ts @@ -41,8 +41,6 @@ async function main() { }) } - console.log() - { // Second call to OpenAI to generate a text response const res = await openai.responses.create({ diff --git a/examples/ts-sdks/openai/bin/weather.ts b/examples/ts-sdks/openai/bin/weather.ts index 93b3cc1c..8ddb8068 100644 --- a/examples/ts-sdks/openai/bin/weather.ts +++ b/examples/ts-sdks/openai/bin/weather.ts @@ -40,8 +40,6 @@ async function main() { }) } - console.log() - { // Second call to OpenAI to generate a text response const res = await openai.chat.completions.create({ @@ -51,7 +49,7 @@ async function main() { tools: searchTool.functions.toolSpecs }) const message = res.choices?.[0]?.message - console.log(message) + console.log(message?.content) } } diff --git a/packages/openapi-utils/src/openapi-parameters-to-json-schema.ts b/packages/openapi-utils/src/openapi-parameters-to-json-schema.ts index 94a483ec..80fee048 100644 --- a/packages/openapi-utils/src/openapi-parameters-to-json-schema.ts +++ b/packages/openapi-utils/src/openapi-parameters-to-json-schema.ts @@ -183,20 +183,26 @@ function getSchema(parameters: any[], type: string) { ...paramSchema, ...handleNullableSchema(param.schema) } + if ('examples' in param) { paramSchema.examples = getExamples(param.examples) } + schema.properties[param.name] = paramSchema } else { if ('examples' in paramSchema) { paramSchema.examples = getExamples(paramSchema.examples) } + schema.properties[param.name] = param.nullable ? handleNullable(paramSchema) : paramSchema } } + // TODO: support openai strict mode by default (all params must be required, + // and optional params without defaults must be nullable) + schema.required = getRequiredParams(params) } diff --git a/packages/tool-client/src/agentic-tool-client.ts b/packages/tool-client/src/agentic-tool-client.ts index 83238f9a..5d5fb449 100644 --- a/packages/tool-client/src/agentic-tool-client.ts +++ b/packages/tool-client/src/agentic-tool-client.ts @@ -63,6 +63,9 @@ export class AgenticToolClient extends AIFunctionsProvider { name: tool.name, description: tool.description ?? '', inputSchema: createJsonSchema(tool.inputSchema), + // TODO: we should make sure all agentic tools support OpenAI strict + // mode by default. + strict: false, execute: async (json) => { return ky .post( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4b70aaa..b2f3dec4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -476,7 +476,7 @@ catalogs: version: 3.25.67 zod-to-json-schema: specifier: ^3.24.5 - version: 3.24.5 + version: 3.24.6 zod-validation-error: specifier: ^3.5.2 version: 3.5.2 @@ -1429,7 +1429,7 @@ importers: version: 5.1.0 zod-to-json-schema: specifier: 'catalog:' - version: 3.24.5(zod@3.25.67) + version: 3.24.6(zod@3.25.67) packages/validators: dependencies: @@ -1473,6 +1473,9 @@ importers: ky: specifier: 'catalog:' version: 1.8.1 + openai-zod-to-json-schema: + specifier: ^1.1.1 + version: 1.1.1(zod@3.25.67) p-throttle: specifier: 'catalog:' version: 6.2.0 @@ -1482,9 +1485,6 @@ importers: zod: specifier: 'catalog:' version: 3.25.67 - zod-to-json-schema: - specifier: 'catalog:' - version: 3.24.5(zod@3.25.67) zod-validation-error: specifier: 'catalog:' version: 3.5.2(zod@3.25.67) @@ -6338,11 +6338,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.25.1: resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -6436,9 +6431,6 @@ packages: peerDependencies: three: '>=0.126.1' - caniuse-lite@1.0.30001723: - resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==} - caniuse-lite@1.0.30001726: resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} @@ -7109,9 +7101,6 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.167: - resolution: {integrity: sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==} - electron-to-chromium@1.5.177: resolution: {integrity: sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==} @@ -9514,8 +9503,8 @@ packages: resolution: {integrity: sha512-8EcOGJk/JXFaoGjeFM53Z3zBnwOpKtZeu5X0wts67WqA1PTnsmwRgUw9aGAsQ5V6cuTfJUv282h1ypFgDGPDSA==} engines: {node: '>=18'} - openai-zod-to-json-schema@1.0.3: - resolution: {integrity: sha512-CFU+KtOmX1dk2nPCZcGYgbrI3YLJJgMSehx1mLbH1A2fsRmZevHzMau6vFIhtkCpHWkGQ3ossA4a0OzVHlGrkw==} + openai-zod-to-json-schema@1.1.1: + resolution: {integrity: sha512-WIsQn2aXqqhRKVoDAQ7UG2W7K6q4FuJL7sn6lrj4bIHC6bbTYNFDfIw10yWCW3GX/zP2Psvubcmcb2NOCnSzsA==} engines: {node: '>=18'} peerDependencies: zod: ^3.23.8 @@ -11644,11 +11633,6 @@ packages: zod-from-json-schema@0.0.5: resolution: {integrity: sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ==} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} - peerDependencies: - zod: ^3.24.1 - zod-to-json-schema@3.24.6: resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: @@ -11713,7 +11697,7 @@ snapshots: delay: 6.0.0 jsonrepair: 3.12.0 ky: 1.8.1 - openai-zod-to-json-schema: 1.0.3(zod@3.25.67) + openai-zod-to-json-schema: 1.1.1(zod@3.25.67) p-throttle: 6.2.0 type-fest: 4.41.0 zod: 3.25.67 @@ -16690,14 +16674,13 @@ snapshots: '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.6.1 + '@types/pg': 8.15.4 '@types/pg@8.15.4': dependencies: '@types/node': 24.0.7 pg-protocol: 1.10.3 pg-types: 2.2.0 - optional: true '@types/pg@8.6.1': dependencies: @@ -17165,8 +17148,8 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.0 - caniuse-lite: 1.0.30001723 + browserslist: 4.25.1 + caniuse-lite: 1.0.30001726 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -17300,13 +17283,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.0: - dependencies: - caniuse-lite: 1.0.30001723 - electron-to-chromium: 1.5.167 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) - browserslist@4.25.1: dependencies: caniuse-lite: 1.0.30001726 @@ -17417,8 +17393,6 @@ snapshots: dependencies: three: 0.177.0 - caniuse-lite@1.0.30001723: {} - caniuse-lite@1.0.30001726: {} cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.177.0)(typescript@5.8.3): @@ -17954,8 +17928,6 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.167: {} - electron-to-chromium@1.5.177: {} email-validator@2.0.4: {} @@ -20778,7 +20750,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001723 + caniuse-lite: 1.0.30001726 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -20983,7 +20955,7 @@ snapshots: dependencies: ky: 1.8.1 - openai-zod-to-json-schema@1.0.3(zod@3.25.67): + openai-zod-to-json-schema@1.1.1(zod@3.25.67): dependencies: zod: 3.25.67 @@ -23074,12 +23046,6 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.1.3(browserslist@4.25.0): - dependencies: - browserslist: 4.25.0 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.1): dependencies: browserslist: 4.25.1 @@ -23473,10 +23439,6 @@ snapshots: dependencies: zod: 3.25.67 - zod-to-json-schema@3.24.5(zod@3.25.67): - dependencies: - zod: 3.25.67 - zod-to-json-schema@3.24.6(zod@3.25.67): dependencies: zod: 3.25.67 diff --git a/stdlib/core/openai-zod-to-json-schema b/stdlib/core/openai-zod-to-json-schema new file mode 100644 index 00000000..a5ef201c --- /dev/null +++ b/stdlib/core/openai-zod-to-json-schema @@ -0,0 +1,2 @@ +../.. |  WARN  `node_modules` is present. Lockfile only installation will make it out-of-date +../.. | Progress: resolved 1, reused 0, downloaded 0, added 0 diff --git a/stdlib/core/package.json b/stdlib/core/package.json index fd784a4a..b7debc60 100644 --- a/stdlib/core/package.json +++ b/stdlib/core/package.json @@ -35,7 +35,7 @@ "delay": "catalog:", "jsonrepair": "catalog:", "ky": "catalog:", - "zod-to-json-schema": "catalog:", + "openai-zod-to-json-schema": "^1.1.1", "p-throttle": "catalog:", "type-fest": "catalog:", "zod-validation-error": "catalog:" diff --git a/stdlib/core/src/create-ai-function.ts b/stdlib/core/src/create-ai-function.ts index 679cfcdc..915c2754 100644 --- a/stdlib/core/src/create-ai-function.ts +++ b/stdlib/core/src/create-ai-function.ts @@ -114,7 +114,7 @@ export function createAIFunction< const args = input.function_call?.arguments assert( args, - `Missing required function_call.arguments for function ${name}` + `Missing required function_call.arguments for function "${name}"` ) return inputAgenticSchema.parse(args) } diff --git a/stdlib/core/src/schema.ts b/stdlib/core/src/schema.ts index 1bf8e88d..76f5dcd8 100644 --- a/stdlib/core/src/schema.ts +++ b/stdlib/core/src/schema.ts @@ -1,4 +1,5 @@ import type { z } from 'zod' +import { jsonrepair } from 'jsonrepair' import type * as types from './types' import { parseStructuredOutput } from './parse-structured-output' @@ -102,7 +103,7 @@ export function asZodOrJsonSchema( export function createJsonSchema( jsonSchema: types.JSONSchema, { - parse = (value) => value as TData, + parse, safeParse, source }: { @@ -111,6 +112,15 @@ export function createJsonSchema( source?: any } = {} ): AgenticSchema { + parse ??= (value: unknown) => { + if (typeof value === 'string') { + value = JSON.parse(jsonrepair(value)) + } + + // TODO: use `cfValidateJsonSchema` from `@agentic/json-schema` here. + return value as TData + } + safeParse ??= (value: unknown) => { try { const result = parse(value) diff --git a/stdlib/core/src/zod-to-json-schema.test.ts b/stdlib/core/src/zod-to-json-schema.test.ts index fd9c3b34..547a1ed8 100644 --- a/stdlib/core/src/zod-to-json-schema.test.ts +++ b/stdlib/core/src/zod-to-json-schema.test.ts @@ -60,4 +60,38 @@ describe('zodToJsonSchema', () => { } }) }) + + test('handles optional properties in strict mode', () => { + const params = zodToJsonSchema( + z.object({ + name: z.string().optional(), + age: z.number().optional().default(10) + }), + { + strict: true + } + ) + + expect(params).toEqual({ + additionalProperties: false, + type: 'object', + required: ['name', 'age'], + properties: { + name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ] + }, + age: { + type: 'number', + default: 10 + } + } + }) + }) }) diff --git a/stdlib/core/src/zod-to-json-schema.ts b/stdlib/core/src/zod-to-json-schema.ts index 0eab4673..5be8d398 100644 --- a/stdlib/core/src/zod-to-json-schema.ts +++ b/stdlib/core/src/zod-to-json-schema.ts @@ -1,5 +1,5 @@ import type { z } from 'zod' -import { zodToJsonSchema as zodToJsonSchemaImpl } from 'zod-to-json-schema' +import { zodToJsonSchema as zodToJsonSchemaImpl } from 'openai-zod-to-json-schema' import type * as types from './types' import { omit } from './utils' @@ -16,7 +16,7 @@ export function zodToJsonSchema( return omit( zodToJsonSchemaImpl(schema, { $refStrategy: 'none', - target: strict ? 'openAi' : undefined + openaiStrictMode: strict }), '$schema', 'default',