feat(experiments): support freeform provider responses

This commit is contained in:
2026-10-13 11:59:47 +01:00
parent 74936851ce
commit 8dcdffce44
2 changed files with 190 additions and 18 deletions
+133
View File
@@ -225,6 +225,139 @@ describe("OllamaLlmProvider chat capability detection", () => {
});
});
describe("Ollama freeform semantics", () => {
it("omits format constraint when outputSchema is explicitly null (chat path)", async () => {
const originalBaseUrl = process.env.OLLAMA_BASE_URL;
const fetchSpy = vi.fn()
.mockResolvedValueOnce({ ok: true, text: async () => "" })
.mockResolvedValueOnce({
ok: true,
json: async () => ({ message: { content: '{"freeform":"output"}' } }),
});
vi.stubGlobal("fetch", fetchSpy);
process.env.OLLAMA_BASE_URL = "http://ollama.test";
try {
__resetChatSupportForTests();
const provider = getProvider();
const result = await provider.generateReconstruction("prompt", "model", null, { signal: undefined });
expect(result.response).toBe("{\"freeform\":\"output\"}");
expect(result.providerApiPath).toBe("/api/chat");
// No format key in request body when schema is explicitly null
const requestBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
expect(requestBody).not.toHaveProperty("format");
} finally {
vi.unstubAllGlobals();
if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL;
else process.env.OLLAMA_BASE_URL = originalBaseUrl;
}
});
it("omits format constraint when outputSchema is explicitly null (generate fallback)", async () => {
const originalBaseUrl = process.env.OLLAMA_BASE_URL;
const fetchSpy = vi.fn()
.mockResolvedValueOnce({ ok: false, status: 501, text: async () => "" })
.mockResolvedValueOnce({
ok: true,
json: async () => ({ response: "freeform text from /api/generate" }),
});
vi.stubGlobal("fetch", fetchSpy);
process.env.OLLAMA_BASE_URL = "http://ollama.test";
try {
__resetChatSupportForTests();
const provider = getProvider();
const result = await provider.generateReconstruction("prompt", "model", null, { signal: undefined });
expect(result.response).toBe("freeform text from /api/generate");
expect(result.providerApiPath).toBe("/api/generate");
// No format key in request body when schema is explicitly null
const requestBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
expect(requestBody).not.toHaveProperty("format");
} finally {
vi.unstubAllGlobals();
if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL;
else process.env.OLLAMA_BASE_URL = originalBaseUrl;
}
});
it("still applies default schema when outputSchema is undefined", async () => {
const originalBaseUrl = process.env.OLLAMA_BASE_URL;
const fetchSpy = vi.fn()
.mockResolvedValueOnce({ ok: true, text: async () => "" })
.mockResolvedValueOnce({
ok: true,
json: async () => ({ message: { content: "{}" } }),
});
vi.stubGlobal("fetch", fetchSpy);
process.env.OLLAMA_BASE_URL = "http://ollama.test";
try {
__resetChatSupportForTests();
await getProvider().generateReconstruction("prompt", "model", undefined, { signal: undefined });
const requestBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
// undefined → fallback to reconstructionJsonSchema (format IS present)
expect(requestBody.format).toBeDefined();
} finally {
vi.unstubAllGlobals();
if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL;
else process.env.OLLAMA_BASE_URL = originalBaseUrl;
}
});
});
describe("OpenAI freeform semantics", () => {
it("uses text format when outputSchema is explicitly null", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",
fetchImpl: vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ output_text: "freeform text response" }),
}),
});
await provider.generateReconstruction("prompt", "model", null);
const request = JSON.parse(
provider.fetchImpl.mock.calls[0][1].body,
);
expect(request.text.format.type).toBe("text");
});
it("returns raw text without JSON recovery when outputSchema is explicitly null", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",
fetchImpl: vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ output_text: "not-json-at-all {broken" }),
}),
});
const result = await provider.generateReconstruction("prompt", "model", null);
expect(result.response).toBe("not-json-at-all {broken");
expect(result.providerApiPath).toBe("/v1/responses");
});
it("still applies json_schema format when outputSchema is undefined", async () => {
const provider = createOpenAIReconstructionProvider({
apiKey: "test-key",
fetchImpl: vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ output_text: "{}" }),
}),
});
await provider.generateReconstruction("prompt");
const request = JSON.parse(provider.fetchImpl.mock.calls[0][1].body);
expect(request.text.format.type).toBe("json_schema");
});
});
describe("OpenAI reconstruction provider experiment seam", () => {
function assertStrictObjectInvariant(schema, root = schema, visited = new Set()) {
if (!schema || typeof schema !== "object" || visited.has(schema)) return;