fix: route plugin LLM completions through Codex runtime (#81511)

* fix: route plugin LLM completions through Codex runtime

* fix: preserve OpenRouter completion model ids

* fix: allow registry config compat guards
This commit is contained in:
Josh Lehman
2026-05-13 21:02:28 -07:00
committed by GitHub
parent 3b8ac38ae9
commit aac216d699
5 changed files with 103 additions and 3 deletions

View File

@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
- Gateway: keep active reply runs visible to stuck-session diagnostics and clear no-active-work recovery state, preventing stale queued lanes after compaction or tool failures. Fixes #80677. (#81302)
- Codex app-server: rotate incompatible context-engine-managed native threads so Lossless-managed sessions do not resume stale hidden Codex history. (#81223) Thanks @jalehman.
- Codex cron: execute scheduled command-style automation payloads before workspace bootstrap or memory review, preserving existing isolated cron jobs after Codex harness migration. (#81510) Thanks @jalehman.
- Plugin LLM completions: honor Codex agent-runtime policy for canonical OpenAI model refs, so context-engine summarizers can use Codex OAuth instead of requiring direct `OPENAI_API_KEY` auth. (#81511) Thanks @jalehman.
- Gateway/OpenAI HTTP: return OpenAI-compatible 400 errors for invalid sampling params and provider validation failures instead of collapsing them to 500s. (#81275) Thanks @Lellansin.
- Telegram: publish plugin and skill command description localizations to native command menus while filtering unsupported locale codes and preserving Telegram command limits. (#81351) Thanks @jzakirov.
- Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987.

View File

@@ -13,6 +13,8 @@ const COMPAT_CONFIG_API_FILES = new Set([
"src/plugin-sdk/config-runtime.ts",
"src/plugin-sdk/memory-core-host-runtime-core.ts",
"src/plugins/compat/registry.ts",
"src/plugins/registry.runtime-config.test.ts",
"src/plugins/registry.ts",
"src/plugins/contracts/config-boundary-guard.test.ts",
"src/plugins/contracts/deprecated-internal-config-api.test.ts",
"src/plugins/registry.runtime-config.test.ts",

View File

@@ -74,6 +74,26 @@ describe("resolveSimpleCompletionSelectionForAgent", () => {
expect(selection.profileId).toBe("work");
});
it("uses Codex execution provider for OpenAI model refs with Codex runtime policy", () => {
const cfg = {
agents: {
defaults: {
model: "openai/gpt-5.4-mini",
models: {
"openai/gpt-5.4-mini": { agentRuntime: { id: "codex" } },
},
},
},
} as OpenClawConfig;
const selection = requireSelection(
resolveSimpleCompletionSelectionForAgent({ cfg, agentId: "main" }),
);
expect(selection.provider).toBe("openai");
expect(selection.modelId).toBe("gpt-5.4-mini");
expect(selection.runtimeProvider).toBe("openai-codex");
});
it("falls back to runtime default model when no explicit model is configured", () => {
const cfg = {} as OpenClawConfig;

View File

@@ -1,5 +1,6 @@
import type { Model } from "@earendil-works/pi-ai";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
const hoisted = vi.hoisted(() => ({
resolveModelMock: vi.fn(),
@@ -41,10 +42,14 @@ vi.mock("../plugins/provider-runtime.runtime.js", () => ({
let completeWithPreparedSimpleCompletionModel: typeof import("./simple-completion-runtime.js").completeWithPreparedSimpleCompletionModel;
let prepareSimpleCompletionModel: typeof import("./simple-completion-runtime.js").prepareSimpleCompletionModel;
let prepareSimpleCompletionModelForAgent: typeof import("./simple-completion-runtime.js").prepareSimpleCompletionModelForAgent;
beforeAll(async () => {
({ completeWithPreparedSimpleCompletionModel, prepareSimpleCompletionModel } =
await import("./simple-completion-runtime.js"));
({
completeWithPreparedSimpleCompletionModel,
prepareSimpleCompletionModel,
prepareSimpleCompletionModelForAgent,
} = await import("./simple-completion-runtime.js"));
});
beforeEach(() => {
@@ -500,6 +505,50 @@ describe("prepareSimpleCompletionModel", () => {
});
});
describe("prepareSimpleCompletionModelForAgent", () => {
it("uses Codex auth provider for OpenAI model refs with Codex runtime policy", async () => {
const cfg = {
agents: {
defaults: {
model: "openai/gpt-5.4-mini",
models: {
"openai/gpt-5.4-mini": { agentRuntime: { id: "codex" } },
},
},
},
} as OpenClawConfig;
hoisted.resolveModelMock.mockReturnValueOnce({
model: {
provider: "openai-codex",
id: "gpt-5.4-mini",
},
authStorage: {
setRuntimeApiKey: hoisted.setRuntimeApiKeyMock,
},
modelRegistry: {},
});
const result = await prepareSimpleCompletionModelForAgent({
cfg,
agentId: "main",
});
expectPreparedModelResult(result);
expect(result.selection.provider).toBe("openai");
expect(result.selection.modelId).toBe("gpt-5.4-mini");
expect(result.selection.runtimeProvider).toBe("openai-codex");
expect(hoisted.resolveModelMock).toHaveBeenCalledWith(
"openai-codex",
"gpt-5.4-mini",
expect.any(String),
cfg,
);
expect(
(callArg(hoisted.getApiKeyForModelMock) as { model?: { provider?: string } }).model?.provider,
).toBe("openai-codex");
});
});
describe("completeWithPreparedSimpleCompletionModel", () => {
it("prepares provider-owned stream APIs before running a completion", async () => {
const model = {

View File

@@ -10,6 +10,7 @@ import { formatErrorMessage } from "../infra/errors.js";
import { prepareProviderRuntimeAuth } from "../plugins/provider-runtime.runtime.js";
import { resolveAgentDir, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
import { DEFAULT_PROVIDER } from "./defaults.js";
import { resolveAgentHarnessPolicy } from "./harness/policy.js";
import {
applyLocalNoAuthHeaderOverride,
getApiKeyForModel,
@@ -21,6 +22,7 @@ import {
resolveDefaultModelForAgent,
resolveModelRefFromString,
} from "./model-selection.js";
import { OPENAI_CODEX_PROVIDER_ID, isOpenAIProvider } from "./openai-codex-routing.js";
import { resolveModel, resolveModelAsync } from "./pi-embedded-runner/model.js";
import { prepareModelForSimpleCompletion } from "./simple-completion-transport.js";
@@ -55,6 +57,8 @@ export type PreparedSimpleCompletionModel =
export type AgentSimpleCompletionSelection = {
provider: string;
modelId: string;
/** Provider used for auth/transport when runtime policy redirects the logical model ref. */
runtimeProvider?: string;
profileId?: string;
agentDir: string;
};
@@ -102,11 +106,35 @@ export function resolveSimpleCompletionSelectionForAgent(params: {
return {
provider,
modelId,
...resolveSimpleCompletionRuntimeProvider({
cfg: params.cfg,
agentId: params.agentId,
provider,
modelId,
}),
profileId: split?.profile || undefined,
agentDir: resolveAgentDir(params.cfg, params.agentId),
};
}
function resolveSimpleCompletionRuntimeProvider(params: {
cfg: OpenClawConfig;
agentId: string;
provider: string;
modelId: string;
}): Pick<AgentSimpleCompletionSelection, "runtimeProvider"> {
if (!isOpenAIProvider(params.provider)) {
return {};
}
const policy = resolveAgentHarnessPolicy({
provider: params.provider,
modelId: params.modelId,
config: params.cfg,
agentId: params.agentId,
});
return policy.runtime === "codex" ? { runtimeProvider: OPENAI_CODEX_PROVIDER_ID } : {};
}
async function setRuntimeApiKeyForCompletion(params: {
authStorage: SimpleCompletionAuthStorage;
model: Model<Api>;
@@ -266,7 +294,7 @@ export async function prepareSimpleCompletionModelForAgent(params: {
}
const prepared = await prepareSimpleCompletionModel({
cfg: params.cfg,
provider: selection.provider,
provider: selection.runtimeProvider ?? selection.provider,
modelId: selection.modelId,
agentDir: selection.agentDir,
profileId: selection.profileId,