refactor(providers): unify lifecycle-safe manifest registration (#114381)

* refactor(providers): canonicalize manifest-driven registration

* fix(zai): eliminate type-only SDK import side effects
This commit is contained in:
Peter Steinberger
2026-07-27 03:20:02 -04:00
committed by GitHub
parent 65cce37480
commit eec2c0cc26
20 changed files with 892 additions and 825 deletions

View File

@@ -78,6 +78,20 @@ describe("opencode-go provider plugin", () => {
clearLiveCatalogCacheForTests();
});
it("registers only the Go auth choice from its own provider manifest", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(provider.id).toBe("opencode-go");
expect(provider.envVars).toEqual(["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]);
expect(provider.auth.map((method) => method.id)).toEqual(["api-key"]);
expect(provider.auth.map((method) => method.wizard?.choiceId)).toEqual(["opencode-go"]);
expect(provider.auth[0]?.wizard).toMatchObject({
choiceLabel: "OpenCode Go catalog",
groupId: "opencode",
groupHint: "Shared API key for Zen + Go catalogs",
});
});
it("registers image media understanding through the OpenCode Go plugin", async () => {
const { mediaProviders } = await registerProviderPlugin({
plugin,

View File

@@ -1,9 +1,9 @@
// Opencode Go plugin entrypoint registers its OpenClaw integration.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { applyOpencodeGoConfig, OPENCODE_GO_DEFAULT_MODEL_REF } from "./api.js";
import { opencodeGoMediaUnderstandingProvider } from "./media-understanding-provider.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
buildOpencodeGoLiveProviderConfig,
buildStaticOpencodeGoProviderConfig,
@@ -18,12 +18,6 @@ import { createOpencodeGoWrapper } from "./stream.js";
const PROVIDER_ID = "opencode-go";
const OPENCODE_SHARED_PROFILE_IDS = ["opencode:default", "opencode-go:default"] as const;
const OPENCODE_SHARED_HINT = "Shared API key for Zen + Go catalogs";
const OPENCODE_SHARED_WIZARD_GROUP = {
groupId: "opencode",
groupLabel: "OpenCode",
groupHint: OPENCODE_SHARED_HINT,
} as const;
type OpencodeGoCatalogAuth = {
apiKey?: string;
discoveryApiKey?: string;
@@ -44,103 +38,90 @@ function resolveOpencodeGoCatalogAuth(
return hasCatalogAuth(sharedOpencodeAuth) ? sharedOpencodeAuth : undefined;
}
export default definePluginEntry({
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "OpenCode Go Provider",
description: "Bundled OpenCode Go provider plugin",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "OpenCode Go",
docsPath: "/providers/models",
envVars: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "OpenCode Go catalog",
hint: OPENCODE_SHARED_HINT,
optionKey: "opencodeGoApiKey",
flagName: "--opencode-go-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
profileIds: [...OPENCODE_SHARED_PROFILE_IDS],
defaultModel: OPENCODE_GO_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyOpencodeGoConfig(cfg),
expectedProviders: ["opencode", "opencode-go"],
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Go focuses on Kimi, GLM, and MiniMax coding models.",
"Get your API key at: https://opencode.ai/auth",
].join("\n"),
noteTitle: "OpenCode",
wizard: {
choiceId: "opencode-go",
choiceLabel: "OpenCode Go catalog",
...OPENCODE_SHARED_WIZARD_GROUP,
},
}),
],
normalizeConfig: ({ providerConfig }) => {
const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({
api: providerConfig.api,
baseUrl: providerConfig.baseUrl,
});
return normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl
? { ...providerConfig, baseUrl: normalizedBaseUrl }
: undefined;
},
normalizeResolvedModel: ({ model }) => {
const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({
api: model.api,
baseUrl: model.baseUrl,
});
const baseUrlNormalized =
normalizedBaseUrl && normalizedBaseUrl !== model.baseUrl
? { ...model, baseUrl: normalizedBaseUrl }
: model;
const modelNormalized = normalizeOpencodeGoResolvedModel(baseUrlNormalized);
if (modelNormalized) {
return modelNormalized;
manifest,
provider: {
label: "OpenCode Go",
docsPath: "/providers/models",
envVars: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
manifestAuth: {
hint: OPENCODE_SHARED_HINT,
promptMessage: "Enter OpenCode API key",
profileIds: [...OPENCODE_SHARED_PROFILE_IDS],
defaultModel: OPENCODE_GO_DEFAULT_MODEL_REF,
applyConfig: applyOpencodeGoConfig,
expectedProviders: ["opencode", "opencode-go"],
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Go focuses on Kimi, GLM, and MiniMax coding models.",
"Get your API key at: https://opencode.ai/auth",
].join("\n"),
noteTitle: "OpenCode",
},
normalizeConfig: ({ providerConfig }) => {
const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({
api: providerConfig.api,
baseUrl: providerConfig.baseUrl,
});
return normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl
? { ...providerConfig, baseUrl: normalizedBaseUrl }
: undefined;
},
normalizeResolvedModel: ({ model }) => {
const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({
api: model.api,
baseUrl: model.baseUrl,
});
const baseUrlNormalized =
normalizedBaseUrl && normalizedBaseUrl !== model.baseUrl
? { ...model, baseUrl: normalizedBaseUrl }
: model;
const modelNormalized = normalizeOpencodeGoResolvedModel(baseUrlNormalized);
if (modelNormalized) {
return modelNormalized;
}
return baseUrlNormalized !== model ? baseUrlNormalized : undefined;
},
normalizeTransport: ({ api: apiLocal, baseUrl }) => {
const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({ api: apiLocal, baseUrl });
return normalizedBaseUrl && normalizedBaseUrl !== baseUrl
? {
api: apiLocal,
baseUrl: normalizedBaseUrl,
}
: undefined;
},
resolveDynamicModel: ({ modelId }) => resolveOpencodeGoModel(modelId),
catalog: {
order: "simple",
run: async (ctx) => {
const auth = resolveOpencodeGoCatalogAuth(ctx.resolveProviderApiKey);
if (!auth) {
return null;
}
return baseUrlNormalized !== model ? baseUrlNormalized : undefined;
},
normalizeTransport: ({ api: apiLocal, baseUrl }) => {
const normalizedBaseUrl = normalizeOpencodeGoBaseUrl({ api: apiLocal, baseUrl });
return normalizedBaseUrl && normalizedBaseUrl !== baseUrl
? {
api: apiLocal,
baseUrl: normalizedBaseUrl,
}
: undefined;
},
resolveDynamicModel: ({ modelId }) => resolveOpencodeGoModel(modelId),
catalog: {
order: "simple",
run: async (ctx) => {
const auth = resolveOpencodeGoCatalogAuth(ctx.resolveProviderApiKey);
if (!auth) {
return null;
}
if (!auth.discoveryApiKey) {
return {
provider: buildStaticOpencodeGoProviderConfig(auth.apiKey),
};
}
if (!auth.discoveryApiKey) {
return {
provider: await buildOpencodeGoLiveProviderConfig({
apiKey: auth.apiKey ?? auth.discoveryApiKey,
discoveryApiKey: auth.discoveryApiKey,
}),
provider: buildStaticOpencodeGoProviderConfig(auth.apiKey),
};
},
}
return {
provider: await buildOpencodeGoLiveProviderConfig({
apiKey: auth.apiKey ?? auth.discoveryApiKey,
discoveryApiKey: auth.discoveryApiKey,
}),
};
},
augmentModelCatalog: () => listOpencodeGoModelCatalogEntries(),
...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }),
resolveThinkingProfile,
wrapStreamFn: (ctx) => createOpencodeGoWrapper(ctx.streamFn, ctx.thinkingLevel),
isModernModelRef: () => true,
});
},
augmentModelCatalog: () => listOpencodeGoModelCatalogEntries(),
...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }),
resolveThinkingProfile,
wrapStreamFn: (ctx) => createOpencodeGoWrapper(ctx.streamFn, ctx.thinkingLevel),
isModernModelRef: () => true,
},
register(api) {
api.registerMediaUnderstandingProvider(opencodeGoMediaUnderstandingProvider);
},
});