Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 7x 7x 7x 7x 7x 7x 7x 7x 5x 5x 5x 5x 5x 5x 5x 5x 6x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 6x 2x 2x 2x 3x 3x 3x 6x 6x 6x 6x 6x 6x 6x 6x 6x 5x 5x 5x 5x 5x 5x 5x 2x 2x 1x 1x 1x 2x 5x 5x 5x 5x 5x 5x 5x 6x 6x 6x 6x 6x 6x 5x 5x 5x 5x 5x 5x 5x 10x 10x 10x 10x 1x 10x 5x 5x 2x 2x 2x 2x 2x | // @ts-check
/**
* @typedef {{ [path: string]: string }} FileMap
* @typedef {{ toFiles: () => FileMap }} FileBackedModel
* @typedef {{ filePath: string, kind: "domain" | "problem" | "model", content: string }} InlineInput
* @typedef {{ apiKey?: string, baseUrl?: string, fetch?: typeof globalThis.fetch }} ClientOptions
* @typedef {{ computeTierId: string, timeLimitS?: number }} SubmitOptions
* @typedef {{ jobId: string, status: string, effectiveTimeLimitS: number, maxCreditCost?: number }} SubmitResult
*/
export class FeasblClient {
/**
* Create a Feasbl API client for direct model submission.
* @param {ClientOptions} [options] Client configuration; `apiKey` defaults to `FEASBL_API_KEY`.
*/
constructor(options = {}) {
/** @type {string | undefined} */
this.apiKey = options.apiKey ?? readApiKey();
/** @type {string} */
this.baseUrl = (options.baseUrl ?? "https://api.feasbl.com").replace(/\/$/, "");
/** @type {typeof globalThis.fetch} */
this.fetch = options.fetch ?? globalThis.fetch;
if (!this.fetch) throw new Error("A fetch implementation is required");
}
/**
* Submit a generated Jia or PDDL model directly to the Feasbl jobs API.
* @param {FileBackedModel} model Object exposing `toFiles()`.
* @param {SubmitOptions} options Compute tier and runtime limits for the job.
* @returns {Promise<SubmitResult>}
*/
async submit(model, options) {
if (!this.apiKey) throw new Error("Missing Feasbl API key");
const body = {
computeTierId: options.computeTierId,
timeLimitS: options.timeLimitS,
inputs: filesToInputs(model.toFiles()).map(input => ({
filePath: input.filePath,
kind: input.kind,
content: input.content,
})),
};
const response = await this.fetch(`${this.baseUrl}/api/v1/jobs`, {
method: "POST",
headers: {
authorization: `Bearer ${this.apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Feasbl job submission failed: ${response.status} ${explainFailure(text)}`);
}
const payload = await response.json();
return {
jobId: String(payload.jobId ?? payload.job_id),
status: String(payload.status),
effectiveTimeLimitS: Number(payload.effectiveTimeLimitS ?? payload.effective_time_limit_s),
maxCreditCost:
payload.maxCreditCost === undefined && payload.max_credit_cost === undefined
? undefined
: Number(payload.maxCreditCost ?? payload.max_credit_cost),
};
}
}
/**
* Add context for common deployment/auth mismatches.
* @param {string} responseText
* @returns {string}
*/
function explainFailure(responseText) {
if (responseText.includes('"reason":"missing_token"')) {
return `${responseText} (the direct SDK job endpoint is probably not deployed at this base URL yet; the request hit a session-auth route)`;
}
return responseText;
}
/**
* Convert generated SDK files into inline job inputs accepted by the API.
* @param {FileMap} files Map from generated file path to file contents.
* @returns {InlineInput[]}
*/
export function filesToInputs(files) {
return Object.entries(files).map(([filePath, content]) => ({
filePath,
kind: inferKind(filePath),
content,
}));
}
/**
* Infer the Feasbl artifact kind from a generated filename.
* @param {string} filePath Generated file path, such as `model.jia` or `domain.pddl`.
* @returns {"domain" | "problem" | "model"}
*/
export function inferKind(filePath) {
const lower = filePath.toLowerCase();
if (lower.endsWith(".jia")) return "model";
if (lower.includes("domain") && lower.endsWith(".pddl")) return "domain";
if (lower.includes("problem") && lower.endsWith(".pddl")) return "problem";
throw new Error(`Cannot infer Feasbl input kind for ${filePath}`);
}
/** @returns {string | undefined} */
function readApiKey() {
const globals = /** @type {Record<string, unknown>} */ (globalThis);
const proc = /** @type {{ env?: { FEASBL_API_KEY?: string } } | undefined} */ (globals.process);
return proc?.env?.FEASBL_API_KEY;
}
|