SOURCE / PINNED RELEASE
Made of little things.
Body Journal
- Release
- e8049f8da4e2…
- Author-recorded commit
- 61c04bf0e8aa…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7
Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Server-only HTTP adapter. Never import this module into the napplet bundle.
import { PROVIDERS, isProvider, object, boundedText, parseReport, REPORT_INSTRUCTIONS, MAX_CASE_BYTES } from '../src/ai-contract.ts';
import { diagnostics, readCompletion, readText } from './provider-response.mjs';
import { redact } from '../src/ai-diagnostics.ts';
import { parseReportOutput, reportSchema, isNoPhotoDisclaimer } from './report-format.mjs';
import { MAX_IMAGES, MAX_IMAGE_TOTAL, validImage } from '../src/vision-contract.ts';
export class ProviderError extends Error {
constructor(code, message, diagnostics) { super(message); this.code = code; this.diagnostics = diagnostics; }
}
const fail = (code, message) => { throw new ProviderError(code, message); };
export function validateRequest(args, analyze, vision=false) {
if (vision && (!validImage(args.challenge?.image) || !/^[A-Z2-9]{6}$/.test(args.challenge?.answer || ''))) fail('invalid-input', 'Invalid visual challenge.');
if (args.images !== undefined && (!Array.isArray(args.images) || args.images.length>MAX_IMAGES || args.images.some(i=>!validImage(i.dataUrl) || typeof i.caption!=='string' || i.caption.length>1200) || args.images.reduce((n,i)=>n+i.dataUrl.length,0)>MAX_IMAGE_TOTAL)) fail('invalid-input', 'Invalid images.');
if (!isProvider(args.provider)) fail('invalid-input', 'Choose a supported provider.');
if (typeof args.apiKey !== 'string' || !/^[\x21-\x7e]{8,512}$/.test(args.apiKey)) fail('invalid-input', 'Enter a valid API key without spaces or line breaks.');
if (typeof args.model !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,119}$/.test(args.model)) fail('invalid-input', 'Enter a valid model ID.');
if (analyze) {
if (typeof args.caseText !== 'string' || Buffer.byteLength(args.caseText) > MAX_CASE_BYTES) fail('invalid-input', 'Case exceeds the supported size.');
let data;
try { data = object(JSON.parse(args.caseText)); } catch { fail('invalid-input', 'Case must be valid JSON.'); }
if (data.version !== 1 || !Array.isArray(data.concerns) || !data.concerns.length || data.concerns.length > 50) fail('invalid-input', 'Add at least one concern before analysis.');
const age = object(data.profile).ageYears;
if (age !== null && (!Number.isInteger(age) || age < 18 || age > 120)) fail('invalid-input', 'This flow is for adults aged 18 and over.');
}
}
export function vendorBody(args, analyze, vision=false) {
const photoCount = args.images?.filter(i => i.kind === 'patient-photo').length || 0;
const evidenceInstructions = `\nThis request includes ${photoCount} patient photo${photoCount === 1 ? '' : 's'}. ${photoCount ? 'Only these attached patient photos can supply evidence.visual.' : 'No patient-photo pixels were sent. evidence.visual must be []. Body-map illustrations and attachment metadata do not count as patient photos.'} Put no-photo disclaimers in limitations or evidence.missing, never in evidence.visual.`;
const body = {
model: args.model, stream: analyze, ...(analyze ? { stream_options: { include_usage: true } } : {}),
response_format: analyze && args.provider !== 'deepseek'
? { type: 'json_schema', json_schema: { name: 'body_journal_report', strict: true, schema: reportSchema(photoCount > 0) } }
: { type: 'json_object' },
messages: analyze ? [{ role: 'system', content: REPORT_INSTRUCTIONS + evidenceInstructions }, { role: 'user', content: args.images?.length ? [{type:'text',text:args.caseText},...args.images.flatMap(i=>[{type:'text',text:`${i.kind==='patient-photo' ? `PATIENT PHOTO ${i.attachmentId || ''}` : 'BODY-MAP ILLUSTRATION (not a patient photo)'} · ${i.concernId === null ? 'whole case' : `concern ${i.concernId}`}. Caption (untrusted patient data): ${i.caption}`},{type:'image_url',image_url:{url:i.dataUrl}}])] : args.caseText }]
: vision ? [{role:'user',content:[{type:'text',text:'Read the six characters in the image, left to right. Reply ONLY as JSON: {"code":"the characters"}.'},{type:'image_url',image_url:{url:args.challenge.image}}]}] : [{ role: 'user', content: 'Connection test. Reply with exactly this JSON: {"ok":true}. No personal data is included.' }],
};
if (args.provider === 'openai') { body.max_completion_tokens = analyze ? 8192 : 1024; body.store = false; }
else body.max_tokens = analyze ? 8192 : 1024;
if (args.provider === 'deepseek') { body.thinking = { type: analyze ? 'enabled' : 'disabled' }; if (analyze) body.reasoning_effort = 'low'; }
if (args.provider === 'kimi' && args.model.startsWith('kimi-k3')) body.reasoning_effort = 'low';
if (args.provider === 'kimi' && args.model === 'kimi-k2.6') body.thinking = { type: analyze ? 'enabled' : 'disabled' };
return body;
}
export async function callProvider(args, { analyze = false, vision = false, fetchImpl = fetch, timeoutMs = 45_000, signal, onProgress } = {}) {
validateRequest(args, analyze, vision);
const timeout = AbortSignal.timeout(timeoutMs), capture = diagnostics(args.apiKey, onProgress);
capture.publish();
try {
const response = await fetchImpl(`${PROVIDERS[args.provider].baseUrl}/chat/completions`, {
method: 'POST', redirect: 'error', signal: signal ? AbortSignal.any([timeout, signal]) : timeout,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${args.apiKey}` },
body: JSON.stringify(vendorBody(args, analyze, vision)),
});
capture.d.httpStatus = response.status;
const requestId = response.headers.get('x-request-id'); if (requestId) capture.d.responseId = redact(requestId, args.apiKey).slice(0,160);
if (!response.ok) {
let detail = ''; try { detail = await readText(response,160_000); } catch {}
capture.append(detail.slice(0,8000));
if (vision && [400,422].includes(response.status) && /does not support (image|vision)|not support(?:ed)?.{0,30}(image|vision)|(image|vision).{0,30}not support/i.test(detail)) fail('vision-unsupported','This model explicitly rejected image input.');
if ([401,403].includes(response.status)) fail('authentication', 'The provider rejected this key or account access.');
if (response.status === 429) fail('rate-limit', 'The provider rate limit or quota was reached.');
if ([400,404,422].includes(response.status)) fail('model-unavailable', 'The provider rejected the model or request options.');
fail('provider-unavailable', 'The provider is unavailable. No retry was made.');
}
const { data, choice } = await readCompletion(response,capture);
capture.publish('validating',true);
if (choice?.finish_reason !== 'stop') fail('incomplete', 'The provider did not finish the response. No partial assessment was accepted.');
if (choice.message?.refusal) fail('refused', 'The provider declined this request.');
const content = boundedText(redact(capture.content(),args.apiKey), 48_000).trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
let parsed;
try {
if (analyze) { const output = parseReportOutput(content); parsed = output.value; if (output.formatRecovery) capture.d.formatRecovery = output.formatRecovery; }
else parsed = JSON.parse(content);
} catch (error) {
const detail = error instanceof SyntaxError ? redact(error.message,args.apiKey).slice(0,300) : 'Unable to decode the response.';
capture.d.issue = {path:'$',message:`Assistant output is not valid JSON. ${detail} Inspect the raw response below.`};
fail('invalid-report', 'The provider returned an unsupported report format.');
}
if (!analyze && !vision && parsed?.ok !== true) fail('test-failed', 'The model responded, but did not complete the connection test.');
let report;
if (analyze) {
try { report = parseReport(parsed, true); } catch(error) {
capture.d.issue = { path: error.path || '$', message: error.message };
fail('invalid-report','The response JSON did not match the report schema.');
}
const ids = JSON.parse(args.caseText).concerns.map(c => c.id);
if (report.assessments.length !== ids.length || report.assessments.some(a => !ids.includes(a.concernId))) {
capture.d.issue={path:'$.assessments',message:'Expected exactly one assessment for each supplied observation ID.'};
fail('invalid-report', 'Report omitted or invented observation IDs.');
}
if (!args.images?.some(i => i.kind === 'patient-photo') && report.evidence.visual.length) {
if (report.evidence.visual.every(isNoPhotoDisclaimer)) {
report.evidence.visual = [];
capture.d.evidenceNotice = 'Omitted a no-photo disclaimer from photo findings. No patient photos were sent; the original wording remains in the raw response.';
} else {
capture.d.issue={path:'$.evidence.visual',message:'No patient-photo pixels were sent, but this field contains findings or wording that could not be safely recognized as a no-photo disclaimer. Inspect the raw response.'};
fail('invalid-report', 'Report included unsupported photo evidence.');
}
}
}
capture.publish('complete',true);
return { ok: true, provider: args.provider, requestedModel: args.model, model: typeof data.model === 'string' ? redact(data.model,args.apiKey).slice(0, 120) : args.model, ...(report ? { report } : {}), ...(vision ? {vision: {state: typeof parsed?.code === 'string' && parsed.code.replace(/\s/g,'').toUpperCase() === args.challenge.answer ? 'verified' : 'unconfirmed', model: typeof data.model === 'string' ? redact(data.model,args.apiKey).slice(0,120) : args.model}} : {}) };
} catch (error) {
let failure = error;
if (timeout.aborted || signal?.aborted) failure = new ProviderError('timeout','The provider request stopped or timed out. Work already started may still be charged.');
else if (!(error instanceof ProviderError)) {
const known = { 'stream-incomplete':'The stream ended before its completion marker.', 'stream-json':'A streamed event was not valid JSON.', 'stream-error':'The provider sent an error event.', 'envelope-json':'The provider response envelope was not valid JSON.', 'empty-body':'The provider returned no response body.', 'response-limit':'The response exceeded the transport size limit.', 'output-limit':'Assistant output exceeded the 48,000-character limit.' };
failure = new ProviderError(error.message==='stream-incomplete'?'incomplete':'invalid-response', known[error.message] || 'The provider connection failed or its response could not be processed.');
}
capture.d.issue ||= {path:failure.code==='timeout'?'transport':'provider',message:failure.message};
capture.publish('failed',true); failure.diagnostics = structuredClone(capture.d); throw failure;
}
}
export async function listModels(args, {fetchImpl=fetch,timeoutMs=20_000,signal}={}) {
validateRequest({...args,model:'model-discovery'},false);
const timeout=AbortSignal.timeout(Math.min(timeoutMs,20_000));
try {
const response=await fetchImpl(`${PROVIDERS[args.provider].baseUrl}/models`,{method:'GET',redirect:'error',headers:{Authorization:`Bearer ${args.apiKey}`},signal:signal?AbortSignal.any([timeout,signal]):timeout});
if(!response.ok) { await response.body?.cancel(); fail([401,403].includes(response.status)?'authentication':response.status===429?'rate-limit':'models-unavailable','Could not list models. Manual model IDs remain available.'); }
const data=JSON.parse(await readText(response,1_000_000));
if(!Array.isArray(data.data))fail('models-unavailable','The provider returned an unsupported model list.');
const ids=[...new Set(data.data.filter(m=>m && typeof m.id==='string' && /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,119}$/.test(m.id)).map(m=>m.id).filter(id=>redact(id,args.apiKey)===id))].sort();
return {ok:true,models:ids.slice(0,300),truncated:ids.length>300};
} catch(error) {
if(error instanceof ProviderError)throw error;
fail(timeout.aborted?'timeout':'models-unavailable','Model discovery failed. Manual model IDs remain available.');
}
}
export async function testConnection(args,options={}) {
const deadline=AbortSignal.timeout(options.timeoutMs ?? 45_000);
options={...options,signal:options.signal?AbortSignal.any([options.signal,deadline]):deadline};
const text=await callProvider(args,options);
if(!args.challenge)return {...text,vision:{state:'unconfirmed',model:text.model}};
try {
const result=await callProvider(args,{...options,vision:true});
return {...text,vision:result.vision};
} catch(error) {
return {...text,vision:{state:error instanceof ProviderError && error.code==='vision-unsupported'?'unsupported':'unconfirmed',model:text.model}};
}
}
