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,54 @@
|
||||
# Lightweight ModelForge clients
|
||||
|
||||
These dependency-free clients are included in the installable release archive. They invoke an exact
|
||||
capability, never a model, node or worker. They use explicit timeouts, bounded responses and typed
|
||||
errors carrying the HTTP status and correlation ID. They do not automatically retry inference or
|
||||
follow redirects with credentials. They are server-side consumer clients, not browser session SDKs.
|
||||
|
||||
For Python 3.11+, copy `python/modelforge_client.py` into your application's import path. For Node.js
|
||||
20+ JavaScript/TypeScript, copy `typescript/modelforge-client.mjs` and its adjacent `.d.mts` declaration
|
||||
file into your source directory. No package registry or dependency installation is required.
|
||||
|
||||
Configure the consumer process:
|
||||
|
||||
```text
|
||||
MODELFORGE_URL=https://modelforge.example.internal
|
||||
MODELFORGE_TOKEN_FILE=/run/secrets/modelforge-token
|
||||
MODELFORGE_CAPABILITY=rag.embedding@1
|
||||
```
|
||||
|
||||
Store only the issued service key in that protected file. HTTP is rejected by default; set
|
||||
`MODELFORGE_ALLOW_HTTP=true` only for an explicitly trusted private lab. Origins cannot contain
|
||||
userinfo, paths, query strings or fragments. TLS certificate verification stays enabled.
|
||||
|
||||
```python
|
||||
from modelforge_client import ModelForgeClient, ModelForgeError
|
||||
|
||||
client = ModelForgeClient.from_env(timeout=60)
|
||||
try:
|
||||
print(client.health()) # Verifies identity and scope; does not start inference.
|
||||
response = client.invoke({"input": ["A non-sensitive integration check"]})
|
||||
except ModelForgeError as error:
|
||||
print(error.code, error.status, error.correlation_id)
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { ModelForgeClient, ModelForgeError } from "./modelforge-client.mjs";
|
||||
|
||||
const client = await ModelForgeClient.fromEnv({ timeoutMs: 60000 });
|
||||
try {
|
||||
console.log(await client.health());
|
||||
const response = await client.invoke({ input: ["A non-sensitive integration check"] });
|
||||
} catch (error) {
|
||||
if (error instanceof ModelForgeError)
|
||||
console.error(error.code, error.status, error.correlationId);
|
||||
else throw error;
|
||||
}
|
||||
```
|
||||
|
||||
Use the payload defined by your selected capability. `health()` proves connectivity, not model
|
||||
health or production eligibility. Do not automatically retry timed-out inference: the server may
|
||||
have admitted the original request. Retain correlation/request IDs for diagnosis.
|
||||
|
||||
Validation: backend `test_project_python_client.py` and
|
||||
`node --test clients/typescript/modelforge-client.test.mjs`, both included in managed full validation.
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Dependency-free ModelForge capability client. Python 3.11+, no automatic inference retries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from http.client import HTTPException
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote, urlsplit
|
||||
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
||||
|
||||
MAX_RESPONSE_BYTES = 8 * 1024 * 1024
|
||||
|
||||
|
||||
class ModelForgeError(Exception):
|
||||
def __init__(self, code: str, message: str, status: int = 0, correlation_id: str | None = None):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.status = status
|
||||
self.correlation_id = correlation_id
|
||||
|
||||
|
||||
class _NoRedirect(HTTPRedirectHandler):
|
||||
def redirect_request(self, req: Any, fp: Any, code: int, msg: str, headers: Any, newurl: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class ModelForgeClient:
|
||||
def __init__(self, base_url: str, token: str, capability: str, *, timeout: float = 60, allow_http: bool = False):
|
||||
url = urlsplit(base_url)
|
||||
if (url.scheme not in ({"http", "https"} if allow_http else {"https"})
|
||||
or not url.hostname or url.username or url.password or url.query or url.fragment
|
||||
or url.path not in {"", "/"}):
|
||||
raise ValueError("Use a gateway origin without userinfo, path, query or fragment; HTTP requires allow_http=True")
|
||||
if not re.fullmatch(r"[a-z][a-z0-9_.]*@[1-9][0-9]*", capability):
|
||||
raise ValueError("Use an exact versioned capability key")
|
||||
if not re.fullmatch(r"mf_[A-Za-z0-9_-]{48}", token):
|
||||
raise ValueError("Use a scoped ModelForge service credential")
|
||||
if not math.isfinite(timeout) or not 0 < timeout <= 300:
|
||||
raise ValueError("timeout must be between 0 and 300 seconds")
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.capability = capability
|
||||
self.timeout = timeout
|
||||
self._token = token
|
||||
self._opener = build_opener(_NoRedirect())
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, *, timeout: float = 60) -> ModelForgeClient:
|
||||
with Path(os.environ["MODELFORGE_TOKEN_FILE"]).open("r", encoding="ascii") as handle:
|
||||
token = handle.read(513).strip()
|
||||
return cls(os.environ["MODELFORGE_URL"], token, os.environ["MODELFORGE_CAPABILITY"],
|
||||
timeout=timeout, allow_http=os.getenv("MODELFORGE_ALLOW_HTTP") == "true")
|
||||
|
||||
def _request(self, suffix: str, body: dict[str, Any] | None = None) -> Any:
|
||||
path = f"/api/v1/capabilities/{quote(self.capability, safe='')}/{suffix}"
|
||||
request = Request(self.base_url + path, data=json.dumps(body).encode() if body is not None else None,
|
||||
headers={"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"},
|
||||
method="POST" if body is not None else "GET")
|
||||
try:
|
||||
response = self._opener.open(request, timeout=self.timeout)
|
||||
except HTTPError as error:
|
||||
correlation = error.headers.get("X-Correlation-ID")
|
||||
try:
|
||||
with error:
|
||||
raw = error.read(MAX_RESPONSE_BYTES + 1)
|
||||
except (TimeoutError, OSError, HTTPException):
|
||||
raise ModelForgeError("TRANSPORT_ERROR", "Gateway response failed or timed out",
|
||||
error.code, correlation) from None
|
||||
try:
|
||||
payload = json.loads(raw) if len(raw) <= MAX_RESPONSE_BYTES else {}
|
||||
except (ValueError, UnicodeError):
|
||||
payload = {}
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
nested_error = payload.get("error")
|
||||
error_payload = nested_error if isinstance(nested_error, dict) else payload
|
||||
raise ModelForgeError(
|
||||
str(error_payload.get("code", "HTTP_ERROR")),
|
||||
str(error_payload.get("message", "Gateway request refused")),
|
||||
error.code,
|
||||
error_payload.get("correlation_id") or correlation,
|
||||
) from None
|
||||
except (URLError, TimeoutError, OSError, HTTPException):
|
||||
raise ModelForgeError("TRANSPORT_ERROR", "Gateway connection failed or timed out") from None
|
||||
try:
|
||||
with response:
|
||||
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
||||
correlation = response.headers.get("X-Correlation-ID")
|
||||
except (TimeoutError, OSError, HTTPException):
|
||||
raise ModelForgeError("TRANSPORT_ERROR", "Gateway response failed or timed out") from None
|
||||
if len(raw) > MAX_RESPONSE_BYTES:
|
||||
raise ModelForgeError("RESPONSE_TOO_LARGE", "Gateway response exceeds the client limit", correlation_id=correlation)
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (ValueError, UnicodeError):
|
||||
raise ModelForgeError("INVALID_RESPONSE", "Gateway response is not JSON", correlation_id=correlation) from None
|
||||
|
||||
def health(self) -> dict[str, Any]:
|
||||
"""Authenticate the exact scope without executing inference."""
|
||||
return self._request("check")
|
||||
|
||||
def invoke(self, payload: dict[str, Any]) -> Any:
|
||||
"""Execute once. The client never changes capability, deployment or scheduling priority."""
|
||||
return self._request("invoke", payload)
|
||||
@@ -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