From 4e63ce8ac8fc17dc4b0ffc415d4d35be76765f16 Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sun, 14 Sep 2025 14:19:37 -0700 Subject: [PATCH] test image json capability --- js/polliLib/src/image.js | 28 ++++++++++++++-- js/polliLib/src/models.js | 17 +++++++++- tests/pollilib-image-json-capability.mjs | 42 ++++++++++++++++++++++++ tests/pollilib-image-json.mjs | 8 +++++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 tests/pollilib-image-json-capability.mjs diff --git a/js/polliLib/src/image.js b/js/polliLib/src/image.js index 3568b89..e0beb4c 100644 --- a/js/polliLib/src/image.js +++ b/js/polliLib/src/image.js @@ -1,5 +1,15 @@ import { getDefaultClient } from './client.js'; +function modelSupportsJson(info) { + if (!info || typeof info !== 'object') return false; + if (info.json === true) return true; + const fields = [info.output, info.outputs, info.formats, info.format]; + for (const f of fields) { + if (Array.isArray(f) && f.some(v => String(v).toLowerCase() === 'json')) return true; + } + return false; +} + const bool = v => (v == null ? undefined : (v ? 'true' : 'false')); const sleep = ms => new Promise(res => setTimeout(res, ms)); @@ -19,9 +29,21 @@ export async function image(prompt, { if (enhance != null) params.enhance = bool(enhance); if (safe != null) params.safe = bool(safe); if (referrer) params.referrer = referrer; - if (json) params.json = 'true'; - const headers = json ? { Accept: 'application/json' } : {}; + let expectJson = false; + const headers = {}; + if (json && model) { + try { + const models = await imageModels(client); + if (modelSupportsJson(models?.[model])) { + params.json = 'true'; + headers.Accept = 'application/json'; + expectJson = true; + } + } catch { + // ignore capability errors and fall back to blob response + } + } const r = await client.get(url, { params, headers }); if (!r.ok) throw new Error(`image error ${r.status}`); @@ -29,7 +51,7 @@ export async function image(prompt, { const ct = r.headers.get('content-type') ?? ''; if (ct.includes('application/json')) { const data = await r.json(); - if (json) return data; + if (expectJson) return data; if (data?.url) { const ir = await client.get(data.url); if (ir.ok) return await ir.blob(); diff --git a/js/polliLib/src/models.js b/js/polliLib/src/models.js index dfa6516..ece9b08 100644 --- a/js/polliLib/src/models.js +++ b/js/polliLib/src/models.js @@ -2,11 +2,26 @@ import { getDefaultClient } from './client.js'; import { imageModels } from './image.js'; import { textModels } from './text.js'; +// Return true if a model description indicates JSON support +export function imageModelSupportsJson(info) { + if (!info || typeof info !== 'object') return false; + if (info.json === true) return true; + const fields = [info.output, info.outputs, info.formats, info.format]; + for (const f of fields) { + if (Array.isArray(f) && f.some(v => String(v).toLowerCase() === 'json')) return true; + } + return false; +} + export async function modelCapabilities(client = getDefaultClient()) { - const [image, text] = await Promise.all([ + const [img, text] = await Promise.all([ imageModels(client).catch(() => ({})), textModels(client).catch(() => ({})), ]); + const image = {}; + for (const [name, info] of Object.entries(img ?? {})) { + image[name] = { ...(info || {}), json: imageModelSupportsJson(info) }; + } return { image, text, audio: text?.['openai-audio'] ?? {} }; } diff --git a/tests/pollilib-image-json-capability.mjs b/tests/pollilib-image-json-capability.mjs new file mode 100644 index 0000000..ac2422f --- /dev/null +++ b/tests/pollilib-image-json-capability.mjs @@ -0,0 +1,42 @@ +import assert from 'assert/strict'; +import { image } from '../js/polliLib/src/image.js'; +import { modelCapabilities } from '../js/polliLib/src/models.js'; + +const client = { + imageBase: 'https://example.com', + async get(url, { params, headers } = {}) { + if (url.endsWith('/models')) { + return { + ok: true, + headers: { get: () => 'application/json' }, + async json() { return { foo: { json: true }, bar: {} }; } + }; + } + if (params.model === 'foo') { + assert.equal(params.json, 'true'); + assert.equal(headers.Accept, 'application/json'); + return { + ok: true, + headers: { get: () => 'application/json' }, + async json() { return { model: params.model }; } + }; + } + assert.equal(params.json, undefined); + assert.equal(headers?.Accept, undefined); + return { + ok: true, + headers: { get: () => 'image/png' }, + async blob() { return { blob: true }; } + }; + } +}; + +const caps = await modelCapabilities(client); +assert.equal(caps.image.foo.json, true); +assert.equal(caps.image.bar.json, false); + +const data = await image('test', { model: 'foo', json: true }, client); +assert.equal(data.model, 'foo'); + +const blob = await image('test', { model: 'bar', json: true }, client); +assert.deepEqual(blob, { blob: true }); diff --git a/tests/pollilib-image-json.mjs b/tests/pollilib-image-json.mjs index 6b748f2..78d02dd 100644 --- a/tests/pollilib-image-json.mjs +++ b/tests/pollilib-image-json.mjs @@ -4,6 +4,14 @@ import { image } from '../js/polliLib/src/image.js'; const client = { imageBase: 'https://example.com', async get(url, { params, headers } = {}) { + if (url.endsWith('/models')) { + return { + ok: true, + headers: { get: () => 'application/json' }, + async json() { return { foo: { json: true } }; } + }; + } + assert.equal(params.json, 'true'); assert.equal(headers.Accept, 'application/json'); return { ok: true,