fix: expose runtime-ready provider auth to plugins (#62753)

This commit is contained in:
Josh Lehman
2026-04-07 19:28:36 -07:00
committed by GitHub
parent 5050017543
commit b8f12d99b2
12 changed files with 273 additions and 2 deletions

View File

@@ -224,11 +224,12 @@ describe("plugin runtime command execution", () => {
},
},
{
name: "exposes runtime.modelAuth with getApiKeyForModel and resolveApiKeyForProvider",
name: "exposes runtime.modelAuth with raw and runtime-ready auth helpers",
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
expect(runtime.modelAuth).toBeDefined();
expectFunctionKeys(runtime.modelAuth as Record<string, unknown>, [
"getApiKeyForModel",
"getRuntimeAuthForModel",
"resolveApiKeyForProvider",
]);
},

View File

@@ -89,6 +89,10 @@ function createRuntimeModelAuth(): PluginRuntime["modelAuth"] {
loadModelAuthRuntime,
(runtime) => runtime.getApiKeyForModel,
);
const getRuntimeAuthForModel = createLazyRuntimeMethod(
loadModelAuthRuntime,
(runtime) => runtime.getRuntimeAuthForModel,
);
const resolveApiKeyForProvider = createLazyRuntimeMethod(
loadModelAuthRuntime,
(runtime) => runtime.resolveApiKeyForProvider,
@@ -99,6 +103,12 @@ function createRuntimeModelAuth(): PluginRuntime["modelAuth"] {
model: params.model,
cfg: params.cfg,
}),
getRuntimeAuthForModel: (params) =>
getRuntimeAuthForModel({
model: params.model,
cfg: params.cfg,
workspaceDir: params.workspaceDir,
}),
resolveApiKeyForProvider: (params) =>
resolveApiKeyForProvider({
provider: params.provider,

View File

@@ -0,0 +1,16 @@
import type { ResolvedProviderAuth } from "../../agents/model-auth-runtime-shared.js";
import type { ProviderRequestTransportOverrides } from "../../agents/provider-request-config.js";
/**
* Runtime-ready auth result exposed to native plugins and context engines.
*
* `source`, `mode`, and `profileId` describe how the original credential was
* resolved. `apiKey` is the request-ready credential after any provider-owned
* runtime exchange, so it may differ from the stored/raw credential.
*/
export type ResolvedProviderRuntimeAuth = Omit<ResolvedProviderAuth, "apiKey"> & {
apiKey?: string;
baseUrl?: string;
request?: ProviderRequestTransportOverrides;
expiresAt?: number;
};

View File

@@ -0,0 +1,118 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const hoisted = vi.hoisted(() => ({
getApiKeyForModel: vi.fn(),
resolveApiKeyForProvider: vi.fn(),
prepareProviderRuntimeAuth: vi.fn(),
}));
vi.mock("../../agents/model-auth.js", () => ({
getApiKeyForModel: hoisted.getApiKeyForModel,
resolveApiKeyForProvider: hoisted.resolveApiKeyForProvider,
}));
vi.mock("../provider-runtime.runtime.js", () => ({
prepareProviderRuntimeAuth: hoisted.prepareProviderRuntimeAuth,
}));
let getRuntimeAuthForModel: typeof import("./runtime-model-auth.runtime.js").getRuntimeAuthForModel;
const MODEL = {
id: "github-copilot/gpt-4o",
provider: "github-copilot",
api: "openai-responses",
baseUrl: "https://api.githubcopilot.com",
};
describe("runtime-model-auth.runtime", () => {
beforeAll(async () => {
({ getRuntimeAuthForModel } = await import("./runtime-model-auth.runtime.js"));
});
beforeEach(() => {
hoisted.getApiKeyForModel.mockReset();
hoisted.resolveApiKeyForProvider.mockReset();
hoisted.prepareProviderRuntimeAuth.mockReset();
});
it("returns provider-prepared runtime auth when the provider transforms credentials", async () => {
hoisted.getApiKeyForModel.mockResolvedValue({
apiKey: "github-device-token",
source: "profile:github-copilot:github",
mode: "token",
profileId: "github-copilot:github",
});
hoisted.prepareProviderRuntimeAuth.mockResolvedValue({
apiKey: "copilot-bearer-token",
baseUrl: "https://api.individual.githubcopilot.com",
expiresAt: 123,
});
await expect(
getRuntimeAuthForModel({
model: MODEL as never,
}),
).resolves.toEqual({
apiKey: "copilot-bearer-token",
source: "profile:github-copilot:github",
mode: "token",
profileId: "github-copilot:github",
baseUrl: "https://api.individual.githubcopilot.com",
expiresAt: 123,
});
expect(hoisted.prepareProviderRuntimeAuth).toHaveBeenCalledWith(
expect.objectContaining({
provider: "github-copilot",
context: expect.objectContaining({
apiKey: "github-device-token",
modelId: "github-copilot/gpt-4o",
provider: "github-copilot",
}),
}),
);
});
it("falls back to raw auth when the provider has no runtime auth hook", async () => {
hoisted.getApiKeyForModel.mockResolvedValue({
apiKey: "plain-api-key",
source: "env:OPENAI_API_KEY",
mode: "api-key",
});
hoisted.prepareProviderRuntimeAuth.mockResolvedValue(undefined);
await expect(
getRuntimeAuthForModel({
model: {
...MODEL,
id: "openai/gpt-5.4",
provider: "openai",
} as never,
}),
).resolves.toEqual({
apiKey: "plain-api-key",
source: "env:OPENAI_API_KEY",
mode: "api-key",
});
});
it("skips provider preparation when raw auth does not expose an apiKey", async () => {
hoisted.getApiKeyForModel.mockResolvedValue({
source: "env:AWS_PROFILE",
mode: "aws-sdk",
});
await expect(
getRuntimeAuthForModel({
model: {
...MODEL,
id: "bedrock/claude-sonnet",
provider: "bedrock",
} as never,
}),
).resolves.toEqual({
source: "env:AWS_PROFILE",
mode: "aws-sdk",
});
expect(hoisted.prepareProviderRuntimeAuth).not.toHaveBeenCalled();
});
});

View File

@@ -1 +1,54 @@
export { getApiKeyForModel, resolveApiKeyForProvider } from "../../agents/model-auth.js";
import type { Api, Model } from "@mariozechner/pi-ai";
import { getApiKeyForModel, resolveApiKeyForProvider } from "../../agents/model-auth.js";
import type { OpenClawConfig } from "../../config/config.js";
import { prepareProviderRuntimeAuth } from "../provider-runtime.runtime.js";
import type { ResolvedProviderRuntimeAuth } from "./model-auth-types.js";
export { getApiKeyForModel, resolveApiKeyForProvider };
/**
* Resolve request-ready auth for a runtime model, applying any provider-owned
* `prepareRuntimeAuth` exchange on top of the standard credential lookup.
*/
export async function getRuntimeAuthForModel(params: {
model: Model<Api>;
cfg?: OpenClawConfig;
workspaceDir?: string;
}): Promise<ResolvedProviderRuntimeAuth> {
const resolvedAuth = await getApiKeyForModel({
model: params.model,
cfg: params.cfg,
});
if (!resolvedAuth.apiKey || resolvedAuth.mode === "aws-sdk") {
return resolvedAuth;
}
const preparedAuth = await prepareProviderRuntimeAuth({
provider: params.model.provider,
config: params.cfg,
workspaceDir: params.workspaceDir,
env: process.env,
context: {
config: params.cfg,
workspaceDir: params.workspaceDir,
env: process.env,
provider: params.model.provider,
modelId: params.model.id,
model: params.model,
apiKey: resolvedAuth.apiKey,
authMode: resolvedAuth.mode,
profileId: resolvedAuth.profileId,
},
});
if (!preparedAuth) {
return resolvedAuth;
}
return {
...resolvedAuth,
...preparedAuth,
apiKey: preparedAuth.apiKey ?? resolvedAuth.apiKey,
};
}

View File

@@ -125,6 +125,12 @@ export type PluginRuntimeCore = {
model: import("@mariozechner/pi-ai").Model<import("@mariozechner/pi-ai").Api>;
cfg?: import("../../config/config.js").OpenClawConfig;
}) => Promise<import("../../agents/model-auth.js").ResolvedProviderAuth>;
/** Resolve request-ready auth for a model, including provider runtime exchanges. */
getRuntimeAuthForModel: (params: {
model: import("@mariozechner/pi-ai").Model<import("@mariozechner/pi-ai").Api>;
cfg?: import("../../config/config.js").OpenClawConfig;
workspaceDir?: string;
}) => Promise<import("./model-auth-types.js").ResolvedProviderRuntimeAuth>;
/** Resolve auth for a provider by name. Only provider and optional cfg are used. */
resolveApiKeyForProvider: (params: {
provider: string;