feat(coding-agent): expose dynamic provider refresh
This commit is contained in:
@@ -18,8 +18,34 @@ function model(id: string): Model<"openai-completions"> {
|
||||
};
|
||||
}
|
||||
|
||||
describe("legacy extension OAuth modifyModels", () => {
|
||||
it("applies the synchronous projection after async credential initialization", async () => {
|
||||
describe("extension provider model lifecycle", () => {
|
||||
it("publishes refreshModels results without forcing ModelsStore persistence", async () => {
|
||||
const modelsStore = new InMemoryModelsStore();
|
||||
const runtime = await ModelRuntime.create({
|
||||
credentials: AuthStorage.inMemory(),
|
||||
modelsStore,
|
||||
modelsPath: null,
|
||||
allowModelNetwork: false,
|
||||
});
|
||||
runtime.registerProvider("extension-dynamic", {
|
||||
baseUrl: "http://localhost:8080/v1",
|
||||
apiKey: "local",
|
||||
api: "openai-completions",
|
||||
refreshModels: async () => [
|
||||
{
|
||||
...model("live"),
|
||||
provider: "extension-dynamic",
|
||||
baseUrl: "http://localhost:8080/v1",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await runtime.refresh({ allowNetwork: false });
|
||||
expect(runtime.getModel("extension-dynamic", "live")).toBeDefined();
|
||||
expect(await modelsStore.read("extension-dynamic")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("applies legacy OAuth modifyModels after async credential initialization", async () => {
|
||||
const runtime = await ModelRuntime.create({
|
||||
credentials: AuthStorage.inMemory({
|
||||
"extension-oauth": {
|
||||
|
||||
@@ -36,15 +36,16 @@ describe("FileModelsStore", () => {
|
||||
const path = join(dir, "models-store.json");
|
||||
const store = new FileModelsStore(path);
|
||||
|
||||
await store.write("one", [model("one", "m1")]);
|
||||
await store.write("two", [model("two", "m2")]);
|
||||
await store.write("one", { models: [model("one", "m1")], checkedAt: 100 });
|
||||
await store.write("two", { models: [model("two", "m2")], checkedAt: 200 });
|
||||
|
||||
const reloaded = new FileModelsStore(path);
|
||||
expect((await reloaded.read("one"))?.map((entry) => entry.id)).toEqual(["m1"]);
|
||||
expect((await reloaded.read("two"))?.map((entry) => entry.id)).toEqual(["m2"]);
|
||||
expect((await reloaded.read("one"))?.models.map((entry) => entry.id)).toEqual(["m1"]);
|
||||
expect((await reloaded.read("one"))?.checkedAt).toBe(100);
|
||||
expect((await reloaded.read("two"))?.models.map((entry) => entry.id)).toEqual(["m2"]);
|
||||
|
||||
await reloaded.delete("one");
|
||||
expect(await reloaded.read("one")).toBeUndefined();
|
||||
expect((await reloaded.read("two"))?.map((entry) => entry.id)).toEqual(["m2"]);
|
||||
expect((await reloaded.read("two"))?.models.map((entry) => entry.id)).toEqual(["m2"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("Radius provider", () => {
|
||||
});
|
||||
|
||||
expect(runtime.getModel(RADIUS_PROVIDER_ID, "auto")).toBeDefined();
|
||||
expect(await modelsStore.read(RADIUS_PROVIDER_ID)).toHaveLength(1);
|
||||
expect((await modelsStore.read(RADIUS_PROVIDER_ID))?.models).toHaveLength(1);
|
||||
expect(vi.mocked(fetch).mock.calls[0]?.[1]?.headers).toMatchObject({ authorization: "Bearer access-token" });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createProvider, InMemoryModelsStore, type Model } from "@earendil-works/pi-ai";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { VERSION } from "../src/config.ts";
|
||||
import { withRemoteCatalog } from "../src/core/remote-catalog-provider.ts";
|
||||
|
||||
function model(id: string): Model<"openai-completions"> {
|
||||
@@ -20,8 +21,8 @@ function model(id: string): Model<"openai-completions"> {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("remote catalog provider", () => {
|
||||
it("parses catalogs keyed by model ID", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
it("parses keyed catalogs, sends version headers, and observes the refresh TTL", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
@@ -47,14 +48,27 @@ describe("remote catalog provider", () => {
|
||||
credential: { type: "api_key" },
|
||||
store: {
|
||||
read: () => store.read(provider.id),
|
||||
write: (models) => store.write(provider.id, models),
|
||||
write: (entry) => store.write(provider.id, entry),
|
||||
delete: () => store.delete(provider.id),
|
||||
},
|
||||
allowNetwork: true,
|
||||
});
|
||||
await provider.refreshModels?.({
|
||||
credential: { type: "api_key" },
|
||||
store: {
|
||||
read: () => store.read(provider.id),
|
||||
write: (entry) => store.write(provider.id, entry),
|
||||
delete: () => store.delete(provider.id),
|
||||
},
|
||||
allowNetwork: true,
|
||||
});
|
||||
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "dynamic"]);
|
||||
expect((await store.read(provider.id))?.map((entry) => entry.id)).toEqual(["dynamic"]);
|
||||
expect((await store.read(provider.id))?.models.map((entry) => entry.id)).toEqual(["dynamic"]);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(fetchSpy.mock.calls[0]?.[1]?.headers).toMatchObject({
|
||||
"User-Agent": expect.stringContaining(`pi/${VERSION}`),
|
||||
});
|
||||
});
|
||||
|
||||
it("treats unimplemented pi.dev catalog routes as an unavailable overlay", async () => {
|
||||
@@ -81,13 +95,13 @@ describe("remote catalog provider", () => {
|
||||
credential: { type: "api_key" },
|
||||
store: {
|
||||
read: () => store.read(provider.id),
|
||||
write: (models) => store.write(provider.id, models),
|
||||
write: (entry) => store.write(provider.id, entry),
|
||||
delete: () => store.delete(provider.id),
|
||||
},
|
||||
allowNetwork: true,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static"]);
|
||||
expect(await store.read(provider.id)).toBeUndefined();
|
||||
expect(await store.read(provider.id)).toMatchObject({ models: [], checkedAt: expect.any(Number) });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user