feat(coding-agent): expose dynamic provider refresh
This commit is contained in:
@@ -1,35 +1,38 @@
|
||||
import type { Api, Model } from "./types.ts";
|
||||
|
||||
export interface ModelsStoreEntry {
|
||||
models: readonly Model<Api>[];
|
||||
/** Unix timestamp of the last completed remote check. */
|
||||
checkedAt?: number;
|
||||
}
|
||||
|
||||
/** Persistent model catalogs keyed by provider ID. */
|
||||
export interface ModelsStore {
|
||||
read(providerId: string): Promise<readonly Model<Api>[] | undefined>;
|
||||
write(providerId: string, models: readonly Model<Api>[]): Promise<void>;
|
||||
read(providerId: string): Promise<ModelsStoreEntry | undefined>;
|
||||
write(providerId: string, entry: ModelsStoreEntry): Promise<void>;
|
||||
delete(providerId: string): Promise<void>;
|
||||
}
|
||||
|
||||
/** ModelsStore scoped to one provider. Providers cannot access other providers' catalogs. */
|
||||
export interface ProviderModelsStore {
|
||||
read(): Promise<readonly Model<Api>[] | undefined>;
|
||||
write(models: readonly Model<Api>[]): Promise<void>;
|
||||
read(): Promise<ModelsStoreEntry | undefined>;
|
||||
write(entry: ModelsStoreEntry): Promise<void>;
|
||||
delete(): Promise<void>;
|
||||
}
|
||||
|
||||
export class InMemoryModelsStore implements ModelsStore {
|
||||
private readonly models = new Map<string, readonly Model<Api>[]>();
|
||||
private readonly entries = new Map<string, ModelsStoreEntry>();
|
||||
|
||||
async read(providerId: string): Promise<readonly Model<Api>[] | undefined> {
|
||||
const models = this.models.get(providerId);
|
||||
return models?.map((model) => structuredClone(model));
|
||||
async read(providerId: string): Promise<ModelsStoreEntry | undefined> {
|
||||
const entry = this.entries.get(providerId);
|
||||
return entry ? structuredClone(entry) : undefined;
|
||||
}
|
||||
|
||||
async write(providerId: string, models: readonly Model<Api>[]): Promise<void> {
|
||||
this.models.set(
|
||||
providerId,
|
||||
models.map((model) => structuredClone(model)),
|
||||
);
|
||||
async write(providerId: string, entry: ModelsStoreEntry): Promise<void> {
|
||||
this.entries.set(providerId, structuredClone(entry));
|
||||
}
|
||||
|
||||
async delete(providerId: string): Promise<void> {
|
||||
this.models.delete(providerId);
|
||||
this.entries.delete(providerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ class ModelsImpl implements MutableModels {
|
||||
if (options.signal?.aborted) return;
|
||||
const store: ProviderModelsStore = {
|
||||
read: () => this.modelsStore.read(provider.id),
|
||||
write: (models) => this.modelsStore.write(provider.id, models),
|
||||
write: (entry) => this.modelsStore.write(provider.id, entry),
|
||||
delete: () => this.modelsStore.delete(provider.id),
|
||||
};
|
||||
let stored: Credential | undefined;
|
||||
@@ -589,7 +589,7 @@ export function createProvider<TApi extends Api = Api>(input: CreateProviderOpti
|
||||
try {
|
||||
const stored = await context.store.read();
|
||||
if (stored) {
|
||||
dynamicModels = stored
|
||||
dynamicModels = stored.models
|
||||
.filter((model) => model.provider === input.id)
|
||||
.map((model) => model as Model<TApi>);
|
||||
}
|
||||
@@ -597,7 +597,7 @@ export function createProvider<TApi extends Api = Api>(input: CreateProviderOpti
|
||||
const refreshed = await fetchModels(context);
|
||||
if (context.signal?.aborted) return;
|
||||
dynamicModels = refreshed;
|
||||
await context.store.write(refreshed);
|
||||
await context.store.write({ models: refreshed, checkedAt: Date.now() });
|
||||
} finally {
|
||||
inflightRefresh = undefined;
|
||||
}
|
||||
|
||||
@@ -37,14 +37,14 @@ export function radiusProvider(options: RadiusProviderOptions = {}): Provider<"p
|
||||
inflightRefresh ??= (async () => {
|
||||
try {
|
||||
const stored = await context.store.read();
|
||||
if (stored) models = stored.filter((model) => model.provider === id) as typeof models;
|
||||
if (stored) models = stored.models.filter((model) => model.provider === id) as typeof models;
|
||||
|
||||
// Import catalogs cached by the pre-ModelsStore Radius implementation.
|
||||
if (!stored && context.credential?.type === "oauth") {
|
||||
const legacy = getRadiusModels(id, context.credential);
|
||||
if (legacy.length > 0) {
|
||||
models = legacy;
|
||||
await context.store.write(legacy);
|
||||
await context.store.write({ models: legacy, checkedAt: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function radiusProvider(options: RadiusProviderOptions = {}): Provider<"p
|
||||
const config = await loadRadiusGatewayConfig(gateway, apiKey, context.signal);
|
||||
if (context.signal?.aborted) return;
|
||||
models = getRadiusModelsFromConfig(id, config);
|
||||
await context.store.write(models);
|
||||
await context.store.write({ models, checkedAt: Date.now() });
|
||||
} finally {
|
||||
inflightRefresh = undefined;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { envApiKeyAuth } from "../src/auth/helpers.ts";
|
||||
import type { AuthContext, AuthEvent } from "../src/auth/types.ts";
|
||||
import { createModels, createProvider } from "../src/models.ts";
|
||||
import { InMemoryModelsStore } from "../src/models-store.ts";
|
||||
import { InMemoryModelsStore, type ModelsStoreEntry } from "../src/models-store.ts";
|
||||
import { builtinModels, builtinProviders } from "../src/providers/all.ts";
|
||||
import { amazonBedrockProvider } from "../src/providers/amazon-bedrock.ts";
|
||||
import { anthropicProvider } from "../src/providers/anthropic.ts";
|
||||
@@ -360,7 +360,7 @@ describe("createProvider", () => {
|
||||
credential: { type: "api_key" as const },
|
||||
store: {
|
||||
read: () => store.read("dynamic"),
|
||||
write: (listed: readonly Model<Api>[]) => store.write("dynamic", listed),
|
||||
write: (entry: ModelsStoreEntry) => store.write("dynamic", entry),
|
||||
delete: () => store.delete("dynamic"),
|
||||
},
|
||||
allowNetwork: true,
|
||||
|
||||
Reference in New Issue
Block a user