Publish curated ITWorx-ModelForge source from 990401737f96c8d46a9f7b0d84994516cadcb6a0
This commit is contained in:
1 parent
7082ab955a
commit
848f0d8f8a
434 files changed
+67298
-4315
No files matched your search
@@ -0,0 +1,17 @@
|
||||
export interface ClientOptions { timeoutMs?: number; allowHttp?: boolean; }
|
||||
export interface Connectivity { client_id: string; project_key: string | null; capability: string; connected: true; correlation_id: string; }
|
||||
export class ModelForgeError extends Error {
|
||||
readonly code: string;
|
||||
readonly status: number;
|
||||
readonly correlationId: string | null;
|
||||
constructor(code: string, message: string, status?: number, correlationId?: string | null);
|
||||
}
|
||||
export class ModelForgeClient {
|
||||
readonly baseUrl: string;
|
||||
readonly capability: string;
|
||||
readonly timeoutMs: number;
|
||||
constructor(baseUrl: string, token: string, capability: string, options?: ClientOptions);
|
||||
static fromEnv(options?: ClientOptions): Promise<ModelForgeClient>;
|
||||
health(): Promise<Connectivity>;
|
||||
invoke<T = unknown>(payload: Record<string, unknown>): Promise<T>;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { open } from "node:fs/promises";
|
||||
|
||||
export class ModelForgeError extends Error {
|
||||
constructor(code, message, status = 0, correlationId = null) {
|
||||
super(message); this.name = "ModelForgeError"; this.code = code;
|
||||
this.status = status; this.correlationId = correlationId;
|
||||
}
|
||||
}
|
||||
|
||||
/** Node.js 20+. Capability-only, bounded requests, no redirects or inference retries. */
|
||||
export class ModelForgeClient {
|
||||
#token;
|
||||
constructor(baseUrl, token, capability, { timeoutMs = 60000, allowHttp = false } = {}) {
|
||||
const url = new URL(baseUrl);
|
||||
if (!((allowHttp && url.protocol === "http:") || url.protocol === "https:") || url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new TypeError("Use a gateway origin; HTTP requires allowHttp: true");
|
||||
if (!/^[a-z][a-z0-9_.]*@[1-9][0-9]*$/.test(capability)) throw new TypeError("Use an exact versioned capability key");
|
||||
if (!/^mf_[A-Za-z0-9_-]{48}$/.test(token)) throw new TypeError("Use a scoped ModelForge service credential");
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 300000) throw new TypeError("timeoutMs must be between 0 and 300000");
|
||||
this.baseUrl = url.origin; this.capability = capability; this.timeoutMs = timeoutMs; this.#token = token;
|
||||
}
|
||||
static async fromEnv(options = {}) {
|
||||
const file = await open(process.env.MODELFORGE_TOKEN_FILE, "r");
|
||||
let token;
|
||||
try { const buffer = Buffer.alloc(513); const { bytesRead } = await file.read(buffer); token = buffer.subarray(0, bytesRead).toString("utf8").trim(); }
|
||||
finally { await file.close(); }
|
||||
return new ModelForgeClient(process.env.MODELFORGE_URL, token, process.env.MODELFORGE_CAPABILITY, { allowHttp: process.env.MODELFORGE_ALLOW_HTTP === "true", ...options });
|
||||
}
|
||||
async #request(suffix, payload) {
|
||||
let response;
|
||||
let raw;
|
||||
try {
|
||||
response = await fetch(`${this.baseUrl}/api/v1/capabilities/${encodeURIComponent(this.capability)}/${suffix}`, {
|
||||
method: payload === undefined ? "GET" : "POST", redirect: "error", credentials: "omit",
|
||||
signal: AbortSignal.timeout(this.timeoutMs),
|
||||
headers: { Authorization: `Bearer ${this.#token}`, "Content-Type": "application/json" },
|
||||
body: payload === undefined ? undefined : JSON.stringify(payload),
|
||||
});
|
||||
const reader = response.body?.getReader();
|
||||
const chunks = []; let size = 0;
|
||||
if (reader) { try { while (true) {
|
||||
const { done, value } = await reader.read(); if (done) break;
|
||||
size += value.length;
|
||||
if (size > 8 * 1024 * 1024) { await reader.cancel(); throw new ModelForgeError("RESPONSE_TOO_LARGE", "Gateway response exceeds the client limit"); }
|
||||
chunks.push(value);
|
||||
} } finally { reader.releaseLock(); } }
|
||||
raw = Buffer.concat(chunks).toString("utf8");
|
||||
} catch (error) {
|
||||
if (error instanceof ModelForgeError) throw error;
|
||||
throw new ModelForgeError("TRANSPORT_ERROR", "Gateway connection failed or timed out");
|
||||
}
|
||||
let data;
|
||||
try { data = JSON.parse(raw); } catch { throw new ModelForgeError("INVALID_RESPONSE", "Gateway response is not JSON", response.status, response.headers.get("X-Correlation-ID")); }
|
||||
if (!response.ok) {
|
||||
const nestedError = data?.error;
|
||||
const error = nestedError !== null && typeof nestedError === "object" ? nestedError : data;
|
||||
const code = typeof error?.code === "string" ? error.code : "HTTP_ERROR";
|
||||
const message = typeof error?.message === "string" ? error.message : "Gateway request refused";
|
||||
throw new ModelForgeError(
|
||||
code,
|
||||
message,
|
||||
response.status,
|
||||
error?.correlation_id ?? response.headers.get("X-Correlation-ID"),
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
health() { return this.#request("check"); }
|
||||
invoke(payload) { return this.#request("invoke", payload); }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { createServer } from "node:http";
|
||||
import { once } from "node:events";
|
||||
import { test } from "node:test";
|
||||
import { ModelForgeClient, ModelForgeError } from "./modelforge-client.mjs";
|
||||
|
||||
test("capability scope, typed errors, no retries, no redirect credential forwarding", async () => {
|
||||
const seen = [];
|
||||
const server = createServer(async (req, res) => {
|
||||
seen.push({ path: req.url, authorization: req.headers.authorization });
|
||||
if (req.method === "GET") { res.end('{"connected":true}'); return; }
|
||||
const chunks = []; for await (const chunk of req) chunks.push(chunk);
|
||||
const body = JSON.parse(Buffer.concat(chunks));
|
||||
if (body.redirect) { res.writeHead(307, { Location: "/must-not-follow" }); res.end(); return; }
|
||||
res.writeHead(503, { "Content-Type": "application/json" });
|
||||
if (body.flat_error) {
|
||||
res.end('{"code":"LEGACY_ERROR","message":"Legacy error","correlation_id":"legacy-case"}');
|
||||
return;
|
||||
}
|
||||
res.end(JSON.stringify({ error: {
|
||||
code: "NO_ELIGIBLE_NODE", message: "No node", correlation_id: "sdk-case",
|
||||
details: { input: "private benchmark input that must remain redacted" },
|
||||
} }));
|
||||
});
|
||||
server.listen(0, "127.0.0.1"); await once(server, "listening");
|
||||
const token = "mf_" + "k".repeat(48);
|
||||
try {
|
||||
const client = new ModelForgeClient(`http://127.0.0.1:${server.address().port}`, token, "rag.embedding@1", { allowHttp: true, timeoutMs: 2000 });
|
||||
assert.deepEqual(await client.health(), { connected: true });
|
||||
await assert.rejects(client.invoke({ input: ["test"] }), (error) => {
|
||||
assert.ok(error instanceof ModelForgeError);
|
||||
assert.equal(error.code, "NO_ELIGIBLE_NODE");
|
||||
assert.equal(error.message, "No node");
|
||||
assert.equal(error.status, 503);
|
||||
assert.equal(error.correlationId, "sdk-case");
|
||||
assert.ok(!error.message.includes("private benchmark input"));
|
||||
return true;
|
||||
});
|
||||
await assert.rejects(client.invoke({ flat_error: true }), (error) =>
|
||||
error instanceof ModelForgeError
|
||||
&& error.code === "LEGACY_ERROR"
|
||||
&& error.message === "Legacy error"
|
||||
&& error.correlationId === "legacy-case");
|
||||
await assert.rejects(client.invoke({ redirect: true }), (error) => error instanceof ModelForgeError && error.code === "TRANSPORT_ERROR");
|
||||
assert.equal(seen.length, 4);
|
||||
assert.ok(seen.every(({ path, authorization }) => path.startsWith("/api/v1/capabilities/rag.embedding%401/") && authorization === `Bearer ${token}`));
|
||||
assert.ok(!JSON.stringify(client).includes(token));
|
||||
} finally { server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); }
|
||||
});
|
||||
|
||||
test("unsafe origins and credential classes fail before network access", () => {
|
||||
for (const origin of ["http://lab.invalid", "https://user:password@lab.invalid", "https://lab.invalid/path", "https://lab.invalid/?q=key"]) assert.throws(() => new ModelForgeClient(origin, "mf_" + "k".repeat(48), "rag.embedding@1"));
|
||||
assert.throws(() => new ModelForgeClient("https://lab.invalid", "mfop_" + "k".repeat(43), "rag.embedding@1"));
|
||||
});
|
||||
Reference in new issue
Block a user