feat(nono): add restrictive core policy contract
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import registerLmStudio from "./index.ts";
|
||||
|
||||
type Registration = {
|
||||
name: string;
|
||||
config: {
|
||||
baseUrl?: string;
|
||||
refreshModels?: (context: {
|
||||
allowNetwork: boolean;
|
||||
signal: AbortSignal;
|
||||
stored?: { models: readonly unknown[] };
|
||||
}) => Promise<unknown[]>;
|
||||
};
|
||||
};
|
||||
|
||||
function captureRegistration(): Registration {
|
||||
let registration: Registration | undefined;
|
||||
registerLmStudio({
|
||||
registerProvider(name: string, config: Registration["config"]) {
|
||||
registration = { name, config };
|
||||
},
|
||||
} as unknown as ExtensionAPI);
|
||||
assert.ok(registration);
|
||||
return registration;
|
||||
}
|
||||
|
||||
test("registers one LM Studio provider at the exact loopback endpoint", () => {
|
||||
const registration = captureRegistration();
|
||||
assert.equal(registration.name, "lm-studio");
|
||||
assert.equal(registration.config.baseUrl, "http://127.0.0.1:1234/v1");
|
||||
});
|
||||
|
||||
test("refresh validates and maps the LM Studio model catalog", async () => {
|
||||
const registration = captureRegistration();
|
||||
assert.ok(registration.config.refreshModels);
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input) => {
|
||||
assert.equal(String(input), "http://127.0.0.1:1234/v1/models");
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: "fixture-model",
|
||||
name: "Fixture Model",
|
||||
context_window: 32768,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
};
|
||||
try {
|
||||
const models = await registration.config.refreshModels({
|
||||
allowNetwork: true,
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
assert.deepEqual(models, [
|
||||
{
|
||||
id: "fixture-model",
|
||||
name: "Fixture Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32768,
|
||||
maxTokens: 4096,
|
||||
compat: {
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
supportsUsageInStreaming: false,
|
||||
maxTokensField: "max_tokens",
|
||||
},
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("refresh reuses stored models when network access is disabled", async () => {
|
||||
const registration = captureRegistration();
|
||||
assert.ok(registration.config.refreshModels);
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("offline refresh attempted network access");
|
||||
};
|
||||
const stored = [{ id: "stored-model" }];
|
||||
try {
|
||||
const models = await registration.config.refreshModels({
|
||||
allowNetwork: false,
|
||||
signal: AbortSignal.timeout(1000),
|
||||
stored: { models: stored },
|
||||
});
|
||||
assert.deepEqual(models, stored);
|
||||
assert.notEqual(models, stored);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("refresh rejects malformed model catalogs", async () => {
|
||||
const registration = captureRegistration();
|
||||
assert.ok(registration.config.refreshModels);
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
new Response(JSON.stringify({ data: "not-an-array" }), { status: 200 });
|
||||
try {
|
||||
await assert.rejects(
|
||||
registration.config.refreshModels({
|
||||
allowNetwork: true,
|
||||
signal: AbortSignal.timeout(1000),
|
||||
}),
|
||||
/invalid \/v1\/models response/,
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const BASE_URL = "http://127.0.0.1:1234/v1";
|
||||
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||
const DEFAULT_MAX_TOKENS = 16_384;
|
||||
|
||||
type LmStudioModel = {
|
||||
id?: unknown;
|
||||
name?: unknown;
|
||||
context_window?: unknown;
|
||||
max_tokens?: unknown;
|
||||
};
|
||||
|
||||
type LmStudioModelsResponse = {
|
||||
data?: LmStudioModel[];
|
||||
};
|
||||
|
||||
function positiveInteger(value: unknown, fallback: number): number {
|
||||
return typeof value === "number" && Number.isInteger(value) && value > 0
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerProvider("lm-studio", {
|
||||
name: "LM Studio",
|
||||
baseUrl: BASE_URL,
|
||||
apiKey: "lm-studio",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
async refreshModels({ allowNetwork, signal, stored }) {
|
||||
if (!allowNetwork) return [...(stored?.models ?? [])];
|
||||
|
||||
const response = await fetch(`${BASE_URL}/models`, { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`LM Studio model discovery failed: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as LmStudioModelsResponse;
|
||||
if (!Array.isArray(payload.data)) {
|
||||
throw new Error("LM Studio returned an invalid /v1/models response");
|
||||
}
|
||||
|
||||
return payload.data.flatMap((model) => {
|
||||
if (typeof model.id !== "string" || model.id.length === 0) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: model.id,
|
||||
name: typeof model.name === "string" ? model.name : model.id,
|
||||
reasoning: false,
|
||||
input: ["text"] as ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: positiveInteger(
|
||||
model.context_window,
|
||||
DEFAULT_CONTEXT_WINDOW,
|
||||
),
|
||||
maxTokens: positiveInteger(model.max_tokens, DEFAULT_MAX_TOKENS),
|
||||
compat: {
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
supportsUsageInStreaming: false,
|
||||
maxTokensField: "max_tokens" as const,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user