Extend provider adapter runtime contract

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 19:34:10 -04:00
1 parent 0388e1003b
commit 203a48060e
3 files changed
+78 -27

No files matched your search

@@ -1,4 +1,5 @@
import { PROVIDER_AUTH_TYPE, type ProviderAuthType } from "../../shared/domain";
import { listProviderModels, type ProviderModelEntry } from "../../shared/provider-models";
export interface ProviderExecutionInput {
authProfile: ProviderAuthProfile;
@@ -21,10 +22,24 @@ export interface ProviderHealthResult {
checkedAt: string;
}
export interface ProviderContext {
authProfile: ProviderAuthProfile;
model: string | null;
}
export interface ProviderRateLimitState {
requestLimit: number | null;
remainingRequests: number | null;
resetAt: string | null;
rawStateJson: string;
}
export interface ProviderAdapter {
providerKey: string;
authType: ProviderAuthType;
parseAuthProfile(secret: string): ProviderAuthProfile;
listModels(ctx: ProviderContext): Promise<ProviderModelEntry[]>;
getRateLimitState(ctx: ProviderContext): Promise<ProviderRateLimitState | null>;
checkHealth(input: ProviderHealthInput): Promise<ProviderHealthResult>;
createResponse?: (input: ProviderExecutionInput) => Promise<ProviderExecutionResult>;
}
@@ -48,6 +63,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
providerKey: "openai",
authType: PROVIDER_AUTH_TYPE.API_KEY,
parseAuthProfile: parseApiKey,
listModels: () => listStaticModels("openai"),
getRateLimitState: providerRateLimitUnknown,
checkHealth: (input) => checkOpenAiHealth(input.authProfile, "https://api.openai.com/v1/models"),
async createResponse(input) {
const profile = requireApiKeyProfile(input.authProfile, "OpenAI");
@@ -66,6 +83,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
providerKey: "anthropic",
authType: PROVIDER_AUTH_TYPE.API_KEY,
parseAuthProfile: parseApiKey,
listModels: () => listStaticModels("anthropic"),
getRateLimitState: providerRateLimitUnknown,
async checkHealth(input) {
const profile = requireApiKeyProfile(input.authProfile, "Anthropic");
await getJson("https://api.anthropic.com/v1/models", {
@@ -108,6 +127,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
providerKey: "azure-openai",
authType: PROVIDER_AUTH_TYPE.CLOUD,
parseAuthProfile: parseAzureProfile,
listModels: () => listStaticModels("azure-openai"),
getRateLimitState: providerRateLimitUnknown,
async checkHealth(input) {
const profile = requireAzureProfile(input.authProfile);
await getJson(`${profile.baseUrl}/openai/deployments?api-version=2024-10-21`, { "api-key": profile.apiKey });
@@ -118,6 +139,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
providerKey: "aws-bedrock",
authType: PROVIDER_AUTH_TYPE.CLOUD,
parseAuthProfile: parseBedrockProfile,
listModels: () => listStaticModels("aws-bedrock"),
getRateLimitState: providerRateLimitUnknown,
checkHealth: async (input) => {
requireBedrockProfile(input.authProfile);
return healthy();
@@ -127,6 +150,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
providerKey: "vertex-ai",
authType: PROVIDER_AUTH_TYPE.CLOUD,
parseAuthProfile: parseVertexProfile,
listModels: () => listStaticModels("vertex-ai"),
getRateLimitState: providerRateLimitUnknown,
checkHealth: async (input) => {
requireVertexProfile(input.authProfile);
return healthy();
@@ -220,6 +245,8 @@ function openAiCompatibleAdapter({
providerKey,
authType: PROVIDER_AUTH_TYPE.API_KEY,
parseAuthProfile: parseApiKey,
listModels: () => listStaticModels(providerKey),
getRateLimitState: providerRateLimitUnknown,
checkHealth: (input) => checkOpenAiHealth(input.authProfile, healthUrl),
async createResponse(input) {
const profile = requireApiKeyProfile(input.authProfile, label);
@@ -250,6 +277,8 @@ function browserTokenAdapter(providerKey: string, authType: ProviderAuthType): P
providerKey,
authType,
parseAuthProfile: authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION ? parseAccountToken : parseAccessToken,
listModels: () => listStaticModels(providerKey),
getRateLimitState: providerRateLimitUnknown,
checkHealth: async (input) => {
if (authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION) requireAccountTokenProfile(input.authProfile);
else requireAccessTokenProfile(input.authProfile);
@@ -373,3 +402,11 @@ function requireVertexProfile(profile: ProviderAuthProfile): Extract<ProviderAut
function healthy(): ProviderHealthResult {
return { checkedAt: new Date().toISOString() };
}
function listStaticModels(providerKey: string): Promise<ProviderModelEntry[]> {
return Promise.resolve(listProviderModels(providerKey));
}
async function providerRateLimitUnknown(): Promise<ProviderRateLimitState | null> {
return null;
}
@@ -242,10 +242,15 @@ export class SdlcService {
const catalogKeys = PROVIDER_CATALOG.map((provider) => provider.key);
const adapterKeys = Object.keys(providerAdapters);
const missing = catalogKeys.filter((key) => !adapterKeys.includes(key));
const runtimeContractReady = PROVIDER_CATALOG.every((provider) => {
const adapter = providerAdapters[provider.key];
return typeof adapter.listModels === "function" && typeof adapter.getRateLimitState === "function";
});
const schemaVersion = count(this.db, "select coalesce(max(version), 0) as count from schema_migrations");
return [
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
{ key: "schema_version", passed: schemaVersion >= 20, value: schemaVersion },
{ key: "provider_runtime_contract", passed: runtimeContractReady, value: runtimeContractReady },
{ key: "schema_version", passed: schemaVersion >= 21, value: schemaVersion },
];
}
@@ -9,6 +9,30 @@ import { SdlcService } from "../../src/main/services/sdlc-service";
import { EVAL_SUITE_KEY, PROVIDER_AUTH_TYPE, SDLC_RUN_STATE } from "../../src/shared/domain";
import { PROVIDER_CATALOG } from "../../src/shared/provider-catalog";
const PROVIDER_SECRET_SAMPLES: Record<string, string> = {
openai: "sk-live-provider",
anthropic: "sk-live-provider",
xai: "sk-live-provider",
groq: "sk-live-provider",
"github-copilot": "ghu_live_provider",
"google-antigravity": "antigravity-session",
"gpt-account": "chatgpt-session",
"azure-openai": JSON.stringify({
baseUrl: "https://acme.openai.azure.com",
deployment: "gpt-5.4",
secret: "azure-key",
}),
"aws-bedrock": JSON.stringify({
region: "us-east-1",
secret: JSON.stringify({ accessKeyId: "AKIA123", secretAccessKey: "aws-secret" }),
}),
"vertex-ai": JSON.stringify({
projectId: "acme-ai",
region: "us-central1",
secret: JSON.stringify({ client_email: "agent@acme.iam.gserviceaccount.com", private_key: "key" }),
}),
};
function createConnection(): DatabaseConnection {
return new DatabaseConnection(join(tmpdir(), `agentcompany-new-contract-${randomUUID()}.db`));
}
@@ -33,32 +57,8 @@ describe("provider contracts", () => {
});
test("parses credential profiles with required fields", () => {
const samples: Record<string, string> = {
openai: "sk-live-provider",
anthropic: "sk-live-provider",
xai: "sk-live-provider",
groq: "sk-live-provider",
"github-copilot": "ghu_live_provider",
"google-antigravity": "antigravity-session",
"gpt-account": "chatgpt-session",
"azure-openai": JSON.stringify({
baseUrl: "https://acme.openai.azure.com",
deployment: "gpt-5.4",
secret: "azure-key",
}),
"aws-bedrock": JSON.stringify({
region: "us-east-1",
secret: JSON.stringify({ accessKeyId: "AKIA123", secretAccessKey: "aws-secret" }),
}),
"vertex-ai": JSON.stringify({
projectId: "acme-ai",
region: "us-central1",
secret: JSON.stringify({ client_email: "agent@acme.iam.gserviceaccount.com", private_key: "key" }),
}),
};
for (const provider of PROVIDER_CATALOG) {
const profile = providerAdapters[provider.key].parseAuthProfile(samples[provider.key]);
const profile = providerAdapters[provider.key].parseAuthProfile(PROVIDER_SECRET_SAMPLES[provider.key]);
if (provider.authType === PROVIDER_AUTH_TYPE.API_KEY) expect(profile.type).toBe("api_key");
if (provider.authType === PROVIDER_AUTH_TYPE.OAUTH_PKCE) expect(profile.type).toBe("access_token");
if (provider.authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION) expect(profile.type).toBe("account_token");
@@ -81,6 +81,15 @@ describe("provider contracts", () => {
}))).toThrow("Azure base URL must use HTTPS.");
});
test("exposes model listing and rate limit adapter contracts", async () => {
for (const provider of PROVIDER_CATALOG) {
const adapter = providerAdapters[provider.key];
const authProfile = adapter.parseAuthProfile(PROVIDER_SECRET_SAMPLES[provider.key]);
await expect(adapter.listModels({ authProfile, model: provider.defaultModel })).resolves.toEqual(provider.models);
await expect(adapter.getRateLimitState({ authProfile, model: provider.defaultModel })).resolves.toBeNull();
}
});
test("runs the seeded contract eval suite", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
@@ -100,7 +109,7 @@ describe("provider contracts", () => {
.get(runId) as { state: string; failed_count: number; result_json: string };
const result = JSON.parse(run.result_json) as { checks: Array<{ key: string; passed: boolean }> };
expect(run).toMatchObject({ state: SDLC_RUN_STATE.PASSED, failed_count: 0 });
expect(result.checks.map((check) => check.key).sort()).toEqual(["provider_adapter_coverage", "schema_version"]);
expect(result.checks.map((check) => check.key).sort()).toEqual(["provider_adapter_coverage", "provider_runtime_contract", "schema_version"]);
expect(result.checks.every((check) => check.passed)).toBe(true);
connection.close();
});