// src/ai.mjs: an answer from an AI model under the plan's AI allowance, with // no key of your own, as the guide Call an AI model under the allowance shows. import { AllowanceRefusal, AllowanceUpstreamError, generateText } from '@turnzero/ai_allowance'; // The live transport, which only the process entry opens: the platform's // origin and this copy's credential, from the settings it receives. A local // run receives the development credential, so the same transport works there. // Where the credential is not set, it sends nothing and says which line writes it. export function liveTransport() { const origin = process.env.TURNZERO_CLOUD_GATEWAY_URL ?? process.env.TURNZERO_CLOUD_API; return (path, init = {}) => { const credential = process.env.TURNZERO_CLOUD_APP_TOKEN; if (!credential) throw new Error('TURNZERO_CLOUD_APP_TOKEN is not set, so no request was sent. Run the provision line again, then start the app again.'); return fetch(origin + path, { ...init, headers: { ...init.headers, authorization: `Bearer ${credential}` } }); }; } // The asker over any transport: the live one, or a test double's. It answers // { answer } for one question, or { unanswered } naming why the model gave // none: allowance_drawn, the month's units are drawn (the refusal // allowance_exhausted); allowance_unavailable, the platform's key is not in // place; or upstream_quota, the provider is out of capacity. export function createAsker(transport) { return async (question) => { try { const answer = await generateText(transport, { system: 'Answer the question in a few sentences.', messages: [{ role: 'user', content: question }], maxOutputTokens: 400, }); const { used, quota } = answer.allowance; if (typeof used === 'number' && typeof quota === 'number') console.log(`AI allowance: ${quota - used} of ${quota} units left before this call`); return { answer: answer.text }; } catch (error) { // Choose the fallback by the refusal's name, never by its message. const refusal = error instanceof AllowanceRefusal ? error.refusal : null; if (refusal === 'allowance_exhausted') return { unanswered: 'allowance_drawn' }; if (refusal === 'allowance_unavailable') return { unanswered: 'allowance_unavailable' }; const providerFull = error instanceof AllowanceUpstreamError && error.quota; if (providerFull) return { unanswered: 'upstream_quota' }; throw error; } }; }