diff --git a/apps/hub/src/inference/openai-responses.test.ts b/apps/hub/src/inference/openai-responses.test.ts new file mode 100644 index 00000000..b5f421f3 --- /dev/null +++ b/apps/hub/src/inference/openai-responses.test.ts @@ -0,0 +1,37 @@ +import { expect, test } from "bun:test"; +import type { LastCycleSource } from "@intx/types/runtime"; +import { createOpenAIResponsesAdapter } from "./openai-responses.ts"; + +const source: LastCycleSource = { + sourceId: "openai-default", + provider: "openai-responses", + model: "gpt-6.1-terra", +}; + +test("tools are sent non-strict so their optional fields stay optional", () => { + const request = createOpenAIResponsesAdapter(source).buildRequest( + [ + { + role: "user", + content: [{ type: "text", text: "Hello" }], + timestamp: 0, + }, + ], + "gpt-6.1-terra", + { + tools: [ + { + name: "read_artifact", + description: "Read a document", + inputSchema: { + type: "object", + properties: { kind: { type: "string" } }, + }, + }, + ], + }, + ); + + const body = JSON.parse(request.body) as { tools: { strict?: boolean }[] }; + expect(body.tools[0]?.strict).toBe(false); +}); diff --git a/apps/hub/src/inference/openai-responses.ts b/apps/hub/src/inference/openai-responses.ts new file mode 100644 index 00000000..2041372c --- /dev/null +++ b/apps/hub/src/inference/openai-responses.ts @@ -0,0 +1,30 @@ +import { createOpenAIResponsesAdapter as createProviderOpenAIResponsesAdapter } from "@corbits/openai-responses"; +import type { AdapterFactory } from "@intx/inference"; + +// OpenAI treats a function tool without `strict` as strict, which makes every property required, so +// optional fields get invented values. A non-strict tool gives up OpenAI's promise that arguments +// match the schema; every tool checks its own arguments, so a bad call fails and is retried instead. +export const createOpenAIResponsesAdapter: AdapterFactory = ( + source, + quirks, +) => { + const adapter = createProviderOpenAIResponsesAdapter(source, quirks); + return { + ...adapter, + buildRequest: (messages, model, options) => { + const request = adapter.buildRequest(messages, model, options); + const body = JSON.parse(request.body) as { tools?: unknown }; + if (!Array.isArray(body.tools)) return request; + return { + ...request, + body: JSON.stringify({ + ...body, + tools: body.tools.map((tool: Record) => ({ + ...tool, + strict: false, + })), + }), + }; + }, + }; +}; diff --git a/apps/hub/src/provisioning/workflow-services.ts b/apps/hub/src/provisioning/workflow-services.ts index 55423c85..709a2f38 100644 --- a/apps/hub/src/provisioning/workflow-services.ts +++ b/apps/hub/src/provisioning/workflow-services.ts @@ -92,7 +92,8 @@ export async function createWorkflowServices(options: { ) ?? [ { provider: "openai-responses", - specifier: import.meta.resolve("@corbits/openai-responses"), + specifier: new URL("../inference/openai-responses.ts", import.meta.url) + .href, export: "createOpenAIResponsesAdapter", }, {