diff --git a/CHANGELOG.md b/CHANGELOG.md index 82142429e04..4751cbe93af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/scripts/lib/config-boundary-guard.mjs b/scripts/lib/config-boundary-guard.mjs index 80b4d23ce51..c79a20a8195 100644 --- a/scripts/lib/config-boundary-guard.mjs +++ b/scripts/lib/config-boundary-guard.mjs @@ -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", diff --git a/src/agents/simple-completion-runtime.selection.test.ts b/src/agents/simple-completion-runtime.selection.test.ts index 39096b307a3..a3c2bedf66c 100644 --- a/src/agents/simple-completion-runtime.selection.test.ts +++ b/src/agents/simple-completion-runtime.selection.test.ts @@ -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; diff --git a/src/agents/simple-completion-runtime.test.ts b/src/agents/simple-completion-runtime.test.ts index f9c13bd031c..ead14d53e13 100644 --- a/src/agents/simple-completion-runtime.test.ts +++ b/src/agents/simple-completion-runtime.test.ts @@ -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 = { diff --git a/src/agents/simple-completion-runtime.ts b/src/agents/simple-completion-runtime.ts index fcece086efc..af6f83a1655 100644 --- a/src/agents/simple-completion-runtime.ts +++ b/src/agents/simple-completion-runtime.ts @@ -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 { + 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; @@ -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,