From 0450cad3cb58184410368b68fb18ef535707fe63 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 5 Nov 2026 15:38:53 -0800 Subject: [PATCH 1/2] fix(hub): stop the model inventing optional tool fields OpenAI treats a function tool without strict as strict, so every property counted as required and the model filled optional fields with made-up values, which failed and could end a coordinator as a doom loop. The OpenAI API adapter now sends its tools non-strict. --- .../src/inference/openai-responses.test.ts | 37 +++++++++++++++++++ apps/hub/src/inference/openai-responses.ts | 31 ++++++++++++++++ .../hub/src/provisioning/workflow-services.ts | 3 +- 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 apps/hub/src/inference/openai-responses.test.ts create mode 100644 apps/hub/src/inference/openai-responses.ts 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..637858be --- /dev/null +++ b/apps/hub/src/inference/openai-responses.ts @@ -0,0 +1,31 @@ +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 +// the model filled optional fields with made-up values ("dummy", " ") and the calls failed. 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", }, { From 7f33e684596b3add8c382afe7396d03f39d1214a Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 5 Nov 2026 18:38:07 -0800 Subject: [PATCH 2/2] refactor(hub): explain the non-strict tool comment by its mechanism --- apps/hub/src/inference/openai-responses.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/hub/src/inference/openai-responses.ts b/apps/hub/src/inference/openai-responses.ts index 637858be..2041372c 100644 --- a/apps/hub/src/inference/openai-responses.ts +++ b/apps/hub/src/inference/openai-responses.ts @@ -2,9 +2,8 @@ import { createOpenAIResponsesAdapter as createProviderOpenAIResponsesAdapter } import type { AdapterFactory } from "@intx/inference"; // OpenAI treats a function tool without `strict` as strict, which makes every property required, so -// the model filled optional fields with made-up values ("dummy", " ") and the calls failed. 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. +// 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,