mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 20:44:47 +00:00
fix: honor Codex auth order for OpenAI PI (#82605)
* fix: honor Codex auth order for OpenAI PI * docs: add PR reference for OpenAI PI auth fix
This commit is contained in:
committed by
GitHub
parent
16e5d6692d
commit
9dedc4d95c
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc.
|
||||
- Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn.
|
||||
- Agents/Codex: fall back to the embedded PI runner when OpenAI's implicit Codex harness preference cannot find a registered Codex plugin, preventing OpenAI-compatible gateway requests from failing with an unregistered harness error. Fixes #82437.
|
||||
- Agents/OpenAI: honor `openai-codex:*` entries placed ahead of API-key backups in `auth.order.openai` for explicit OpenAI PI runs, and accept `models auth login --provider openai-codex --device-code` for headless sign-in. Fixes #82521. (#82605)
|
||||
- CLI/channels: install missing externalized same-id channel plugins during `channels add --channel <id>`, so recovery for WhatsApp and other externalized stock channels does not require a separate `plugins enable` step. Fixes #82533.
|
||||
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant.
|
||||
- Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance.
|
||||
|
||||
@@ -894,6 +894,7 @@ async function agentCommandInternal(
|
||||
const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider: providerForAuthProfileValidation,
|
||||
harnessRuntime: validationHarnessPolicy.runtime,
|
||||
config: cfg,
|
||||
}).map((candidateProvider) =>
|
||||
resolveProviderIdForAuth(candidateProvider, { config: cfg, workspaceDir }),
|
||||
);
|
||||
|
||||
@@ -337,6 +337,40 @@ describe("resolveAuthProfileOrder", () => {
|
||||
expect(order).toEqual(["openai-codex:personal", "openai:default"]);
|
||||
});
|
||||
|
||||
it("keeps Codex profiles listed in the friendly OpenAI order for Codex auth", async () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:personal": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access",
|
||||
refresh: "refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
"openai:backup": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-platform",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:personal", "openai:backup"],
|
||||
},
|
||||
},
|
||||
},
|
||||
store,
|
||||
provider: "openai-codex",
|
||||
});
|
||||
|
||||
expect(order).toEqual(["openai-codex:personal", "openai:backup"]);
|
||||
});
|
||||
|
||||
it("keeps direct OpenAI Codex auth order ahead of the friendly OpenAI alias", async () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
|
||||
@@ -256,6 +256,7 @@ async function resolveRuntimeModel(params: {
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
}).runtime,
|
||||
config: params.cfg,
|
||||
}),
|
||||
agentDir: params.agentDir,
|
||||
sessionEntry: params.sessionEntry,
|
||||
|
||||
@@ -463,6 +463,11 @@ export function runAgentAttempt(params: {
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
const embeddedPiHarnessOverride =
|
||||
requestedAgentHarnessId ??
|
||||
(agentHarnessPolicy.runtime === "pi" && embeddedPiProvider !== params.providerOverride
|
||||
? "pi"
|
||||
: undefined);
|
||||
if (!isRawModelRun && isCliProvider(cliExecutionProvider, params.cfg)) {
|
||||
const cliSessionBinding = getCliSessionBinding(params.sessionEntry, cliExecutionProvider);
|
||||
const resolveReusableCliSessionBinding = async () => {
|
||||
@@ -609,7 +614,8 @@ export function runAgentAttempt(params: {
|
||||
sessionFile: params.sessionFile,
|
||||
workspaceDir: params.workspaceDir,
|
||||
config: params.cfg,
|
||||
agentHarnessId: requestedAgentHarnessId,
|
||||
agentHarnessId: embeddedPiHarnessOverride,
|
||||
agentHarnessRuntimeOverride: embeddedPiHarnessOverride,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
prompt: effectivePrompt,
|
||||
images: params.isFallbackRetry ? undefined : params.opts.images,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveAuthProfileDisplayLabel,
|
||||
resolveAuthProfileOrder,
|
||||
} from "./auth-profiles.js";
|
||||
import { isStoredCredentialCompatibleWithAuthProvider } from "./auth-profiles/order.js";
|
||||
import {
|
||||
readClaudeCliCredentialsCached,
|
||||
readCodexCliCredentialsCached,
|
||||
@@ -21,6 +22,7 @@ export function resolveModelAuthLabel(params: {
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
includeExternalProfiles?: boolean;
|
||||
acceptedProviderIds?: readonly string[];
|
||||
}): string | undefined {
|
||||
const resolvedProvider = params.provider?.trim();
|
||||
if (!resolvedProvider) {
|
||||
@@ -39,17 +41,37 @@ export function resolveModelAuthLabel(params: {
|
||||
}),
|
||||
});
|
||||
const profileOverride = params.sessionEntry?.authProfileOverride?.trim();
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: params.cfg,
|
||||
store,
|
||||
provider: providerKey,
|
||||
preferredProfile: profileOverride,
|
||||
});
|
||||
const acceptedProviderKeys = [
|
||||
...new Set(
|
||||
[...(params.acceptedProviderIds ?? []).map(normalizeProviderId), providerKey].filter(Boolean),
|
||||
),
|
||||
];
|
||||
const order = [
|
||||
...new Set(
|
||||
acceptedProviderKeys.flatMap((acceptedProvider) =>
|
||||
resolveAuthProfileOrder({
|
||||
cfg: params.cfg,
|
||||
store,
|
||||
provider: acceptedProvider,
|
||||
preferredProfile: profileOverride,
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
const candidates = [profileOverride, ...order].filter(Boolean) as string[];
|
||||
|
||||
for (const profileId of candidates) {
|
||||
const profile = store.profiles[profileId];
|
||||
if (!profile || normalizeProviderId(profile.provider) !== providerKey) {
|
||||
if (
|
||||
!profile ||
|
||||
!acceptedProviderKeys.some((acceptedProvider) =>
|
||||
isStoredCredentialCompatibleWithAuthProvider({
|
||||
cfg: params.cfg,
|
||||
provider: acceptedProvider,
|
||||
credential: profile,
|
||||
}),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const label = resolveAuthProfileDisplayLabel({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
modelSelectionShouldEnsureCodexPlugin,
|
||||
openAIProviderUsesCodexRuntimeByDefault,
|
||||
resolveOpenAIRuntimeProviderForPi,
|
||||
resolveSelectedOpenAIPiRuntimeProvider,
|
||||
} from "./openai-codex-routing.js";
|
||||
|
||||
describe("OpenAI Codex routing policy", () => {
|
||||
@@ -51,6 +52,96 @@ describe("OpenAI Codex routing policy", () => {
|
||||
).toBe("openai-codex");
|
||||
});
|
||||
|
||||
it("keeps explicit OpenAI PI Codex auth order ahead of API-key backups", () => {
|
||||
const config = {
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:work", "openai:backup"],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
expect(
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toEqual(["openai-codex", "openai"]);
|
||||
expect(
|
||||
resolveSelectedOpenAIPiRuntimeProvider({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toBe("openai-codex");
|
||||
expect(
|
||||
resolveOpenAIRuntimeProviderForPi({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toBe("openai");
|
||||
});
|
||||
|
||||
it("keeps explicit OpenAI PI API-key auth order ahead of Codex backups", () => {
|
||||
const config = {
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai:backup", "openai-codex:work"],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
expect(
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toEqual(["openai", "openai-codex"]);
|
||||
expect(
|
||||
resolveSelectedOpenAIPiRuntimeProvider({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toBe("openai");
|
||||
});
|
||||
|
||||
it("does not route custom OpenAI-compatible PI configs through Codex auth order", () => {
|
||||
const config = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://proxy.example.test/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:work", "openai:backup"],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
expect(
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toEqual(["openai", "openai-codex"]);
|
||||
expect(
|
||||
resolveSelectedOpenAIPiRuntimeProvider({
|
||||
provider: "openai",
|
||||
harnessRuntime: "pi",
|
||||
config,
|
||||
}),
|
||||
).toBe("openai");
|
||||
});
|
||||
|
||||
it("validates Codex harness auth through the Codex provider contract", () => {
|
||||
expect(
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js";
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
export const OPENAI_PROVIDER_ID = "openai";
|
||||
export const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
@@ -78,6 +78,20 @@ export function hasOpenAICodexAuthProfileOverride(value: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function configuredOpenAIAuthOrderStartsWithCodexProfile(config: OpenClawConfig | undefined) {
|
||||
if (!openAIProviderUsesCodexRuntimeByDefault({ provider: OPENAI_PROVIDER_ID, config })) {
|
||||
return false;
|
||||
}
|
||||
const configuredOpenAIOrder = findNormalizedProviderValue(
|
||||
config?.auth?.order,
|
||||
OPENAI_PROVIDER_ID,
|
||||
);
|
||||
const firstProfile = configuredOpenAIOrder?.find(
|
||||
(profileId) => typeof profileId === "string" && profileId.trim().length > 0,
|
||||
);
|
||||
return hasOpenAICodexAuthProfileOverride(firstProfile);
|
||||
}
|
||||
|
||||
export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: {
|
||||
provider: string;
|
||||
harnessRuntime?: string;
|
||||
@@ -87,16 +101,16 @@ export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): boolean {
|
||||
if (
|
||||
!isOpenAIProvider(params.provider) ||
|
||||
!hasOpenAICodexAuthProfileOverride(params.authProfileId)
|
||||
) {
|
||||
if (!isOpenAIProvider(params.provider)) {
|
||||
return false;
|
||||
}
|
||||
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
|
||||
if (runtime !== "pi") {
|
||||
return false;
|
||||
}
|
||||
if (!hasOpenAICodexAuthProfileOverride(params.authProfileId)) {
|
||||
return false;
|
||||
}
|
||||
const aliasLookupParams = {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -112,6 +126,7 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: {
|
||||
provider: string;
|
||||
harnessRuntime?: string;
|
||||
agentHarnessId?: string;
|
||||
config?: OpenClawConfig;
|
||||
}): string[] {
|
||||
if (!isOpenAIProvider(params.provider)) {
|
||||
return [params.provider];
|
||||
@@ -123,6 +138,9 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: {
|
||||
return [OPENAI_CODEX_PROVIDER_ID];
|
||||
}
|
||||
if (runtime === "pi") {
|
||||
if (configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)) {
|
||||
return [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID];
|
||||
}
|
||||
return [OPENAI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID];
|
||||
}
|
||||
return [params.provider];
|
||||
@@ -150,6 +168,27 @@ export function resolveOpenAIRuntimeProviderForPi(params: {
|
||||
: params.provider;
|
||||
}
|
||||
|
||||
export function resolveSelectedOpenAIPiRuntimeProvider(params: {
|
||||
provider: string;
|
||||
harnessRuntime?: string;
|
||||
agentHarnessId?: string;
|
||||
authProfileProvider?: string;
|
||||
authProfileId?: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): string {
|
||||
if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) {
|
||||
return OPENAI_CODEX_PROVIDER_ID;
|
||||
}
|
||||
const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime);
|
||||
return isOpenAIProvider(params.provider) &&
|
||||
runtime === "pi" &&
|
||||
!params.authProfileId?.trim() &&
|
||||
configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)
|
||||
? OPENAI_CODEX_PROVIDER_ID
|
||||
: params.provider;
|
||||
}
|
||||
|
||||
export function resolveContextConfigProviderForRuntime(params: {
|
||||
provider: string;
|
||||
runtimeId?: string;
|
||||
|
||||
@@ -343,6 +343,69 @@ describe("runEmbeddedPiAgent", () => {
|
||||
expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves explicit OpenAI PI runs through Codex when auth order starts with Codex OAuth", async () => {
|
||||
const sessionFile = nextSessionFile();
|
||||
const cfg = {
|
||||
...createEmbeddedPiRunnerOpenAiConfig(["mock-1"]),
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/mock-1": {
|
||||
agentRuntime: { id: "pi" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:work", "openai:backup"],
|
||||
},
|
||||
},
|
||||
};
|
||||
runEmbeddedAttemptMock.mockResolvedValueOnce(
|
||||
makeEmbeddedRunnerAttempt({
|
||||
assistantTexts: ["ok"],
|
||||
lastAssistant: buildEmbeddedRunnerAssistant({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await runEmbeddedPiAgent({
|
||||
sessionId: "codex-first-pi",
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
prompt: "hello",
|
||||
provider: "openai",
|
||||
model: "mock-1",
|
||||
timeoutMs: 5_000,
|
||||
agentDir,
|
||||
runId: nextRunId("codex-first-pi"),
|
||||
enqueue: immediateEnqueue,
|
||||
});
|
||||
|
||||
expect(resolveModelAsyncMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"openai",
|
||||
"mock-1",
|
||||
agentDir,
|
||||
cfg,
|
||||
expect.objectContaining({ skipPiDiscovery: true }),
|
||||
);
|
||||
expect(resolveModelAsyncMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"openai-codex",
|
||||
"mock-1",
|
||||
agentDir,
|
||||
cfg,
|
||||
expect.objectContaining({ skipPiDiscovery: true }),
|
||||
);
|
||||
expect(
|
||||
(firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider,
|
||||
).toBe("openai-codex");
|
||||
});
|
||||
|
||||
it("backfills a trimmed session key from sessionId when the embedded run omits it", async () => {
|
||||
const sessionFile = nextSessionFile();
|
||||
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
|
||||
|
||||
@@ -61,7 +61,11 @@ import {
|
||||
shouldPreferExplicitConfigApiKeyAuth,
|
||||
} from "../model-auth.js";
|
||||
import { ensureOpenClawModelsJson } from "../models-config.js";
|
||||
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
|
||||
import {
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime,
|
||||
resolveContextConfigProviderForRuntime,
|
||||
resolveSelectedOpenAIPiRuntimeProvider,
|
||||
} from "../openai-codex-routing.js";
|
||||
import {
|
||||
retireSessionMcpRuntime,
|
||||
retireSessionMcpRuntimeForSessionKey,
|
||||
@@ -552,6 +556,16 @@ export async function runEmbeddedPiAgent(
|
||||
agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride,
|
||||
});
|
||||
const pluginHarnessOwnsTransport = agentHarness.id !== "pi";
|
||||
const modelConfigProvider = provider;
|
||||
const selectedPiRuntimeProvider = resolveSelectedOpenAIPiRuntimeProvider({
|
||||
provider,
|
||||
harnessRuntime: agentHarness.id,
|
||||
agentHarnessId: agentHarness.id,
|
||||
authProfileProvider: params.authProfileId?.split(":", 1)[0],
|
||||
authProfileId: params.authProfileId,
|
||||
config: params.config,
|
||||
workspaceDir: resolvedWorkspace,
|
||||
});
|
||||
const dynamicModelResolution = await resolveModelAsync(
|
||||
provider,
|
||||
modelId,
|
||||
@@ -565,7 +579,7 @@ export async function runEmbeddedPiAgent(
|
||||
workspaceDir: resolvedWorkspace,
|
||||
},
|
||||
);
|
||||
const modelResolution =
|
||||
let modelResolution =
|
||||
dynamicModelResolution.model || pluginHarnessOwnsTransport
|
||||
? dynamicModelResolution
|
||||
: await (async () => {
|
||||
@@ -576,6 +590,22 @@ export async function runEmbeddedPiAgent(
|
||||
workspaceDir: resolvedWorkspace,
|
||||
});
|
||||
})();
|
||||
if (selectedPiRuntimeProvider !== provider && modelResolution.model) {
|
||||
const runtimeModelResolution = await resolveModelAsync(
|
||||
selectedPiRuntimeProvider,
|
||||
modelId,
|
||||
agentDir,
|
||||
params.config,
|
||||
{
|
||||
skipPiDiscovery: true,
|
||||
workspaceDir: resolvedWorkspace,
|
||||
},
|
||||
);
|
||||
if (runtimeModelResolution.model) {
|
||||
provider = selectedPiRuntimeProvider;
|
||||
modelResolution = runtimeModelResolution;
|
||||
}
|
||||
}
|
||||
const { model, error, authStorage, modelRegistry } = modelResolution;
|
||||
if (!model) {
|
||||
throw new FailoverError(error ?? `Unknown model: ${provider}/${modelId}`, {
|
||||
@@ -592,7 +622,7 @@ export async function runEmbeddedPiAgent(
|
||||
cfg: params.config,
|
||||
provider,
|
||||
contextConfigProvider: resolveContextConfigProviderForRuntime({
|
||||
provider,
|
||||
provider: modelConfigProvider,
|
||||
runtimeId: agentHarness.id,
|
||||
}),
|
||||
modelId,
|
||||
@@ -719,12 +749,23 @@ export async function runEmbeddedPiAgent(
|
||||
}
|
||||
const profileOrder = shouldPreferExplicitConfigApiKeyAuth(params.config, provider)
|
||||
? []
|
||||
: resolveAuthProfileOrder({
|
||||
cfg: params.config,
|
||||
store: authStore,
|
||||
provider,
|
||||
preferredProfile: preferredProfileId,
|
||||
});
|
||||
: [
|
||||
...new Set(
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider,
|
||||
harnessRuntime: agentHarness.id,
|
||||
agentHarnessId: agentHarness.id,
|
||||
config: params.config,
|
||||
}).flatMap((authProvider) =>
|
||||
resolveAuthProfileOrder({
|
||||
cfg: params.config,
|
||||
store: authStore,
|
||||
provider: authProvider,
|
||||
preferredProfile: preferredProfileId,
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
const providerPreferredProfileId = lockedProfileId
|
||||
? undefined
|
||||
: resolveProviderAuthProfileId({
|
||||
|
||||
@@ -1791,6 +1791,11 @@ export async function runAgentTurnWithFallback(params: {
|
||||
config: runtimeConfig,
|
||||
workspaceDir: params.followupRun.run.workspaceDir,
|
||||
});
|
||||
const embeddedRunHarnessOverride =
|
||||
sessionRuntimeOverride ??
|
||||
(agentHarnessPolicy.runtime === "pi" && embeddedRunProvider !== provider
|
||||
? "pi"
|
||||
: undefined);
|
||||
return (async () => {
|
||||
let attemptCompactionCount = 0;
|
||||
const lifecycleBackstop = createEmbeddedLifecycleTerminalBackstop({
|
||||
@@ -1810,8 +1815,8 @@ export async function runAgentTurnWithFallback(params: {
|
||||
...senderContext,
|
||||
...runBaseParams,
|
||||
provider: embeddedRunProvider,
|
||||
agentHarnessId: sessionRuntimeOverride,
|
||||
agentHarnessRuntimeOverride: sessionRuntimeOverride,
|
||||
agentHarnessId: embeddedRunHarnessOverride,
|
||||
agentHarnessRuntimeOverride: embeddedRunHarnessOverride,
|
||||
sandboxSessionKey: params.runtimePolicySessionKey,
|
||||
prompt: params.commandBody,
|
||||
transcriptPrompt: params.transcriptCommandBody,
|
||||
|
||||
@@ -716,6 +716,87 @@ describe("buildStatusReply subagent summary", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses Codex OAuth auth labels for explicit OpenAI PI auth order", async () => {
|
||||
await withTempHome(
|
||||
async (dir) => {
|
||||
const authPath = path.join(
|
||||
dir,
|
||||
".openclaw",
|
||||
"agents",
|
||||
"main",
|
||||
"agent",
|
||||
"auth-profiles.json",
|
||||
);
|
||||
fs.mkdirSync(path.dirname(authPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:status": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60 * 60_000,
|
||||
},
|
||||
"openai:backup": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-test",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const text = await buildStatusText({
|
||||
cfg: {
|
||||
...baseCfg,
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-5.5": {
|
||||
agentRuntime: { id: "pi" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai-codex:status", "openai:backup"],
|
||||
},
|
||||
},
|
||||
},
|
||||
sessionEntry: {
|
||||
sessionId: "sess-status-openai-pi-codex-oauth",
|
||||
updatedAt: 0,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
parentSessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
statusChannel: "mobilechat",
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
contextTokens: 32_000,
|
||||
resolvedHarness: "pi",
|
||||
resolvedFastMode: false,
|
||||
resolvedVerboseLevel: "off",
|
||||
resolvedReasoningLevel: "off",
|
||||
resolveDefaultThinkingLevel: async () => undefined,
|
||||
isGroup: false,
|
||||
defaultGroupActivation: () => "mention",
|
||||
});
|
||||
|
||||
const normalized = normalizeTestText(text);
|
||||
expect(normalized).toContain("Model: openai/gpt-5.5");
|
||||
expect(normalized).toContain("oauth (openai-codex:status)");
|
||||
expect(normalized).not.toContain("api-key (openai:backup)");
|
||||
},
|
||||
{ env: { OPENAI_API_KEY: undefined } },
|
||||
);
|
||||
});
|
||||
|
||||
it("uses Claude CLI OAuth auth labels for anthropic models running on the Claude CLI runtime", async () => {
|
||||
await withTempHome(
|
||||
async (dir) => {
|
||||
|
||||
@@ -880,6 +880,7 @@ export async function runPreparedReply(
|
||||
? listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider,
|
||||
harnessRuntime: agentHarnessPolicy.runtime,
|
||||
config: cfg,
|
||||
})
|
||||
: [provider];
|
||||
const resolveActiveSessionProviderForAuthProfile = (): string => {
|
||||
|
||||
@@ -328,6 +328,7 @@ export async function createModelSelectionState(params: {
|
||||
const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider,
|
||||
harnessRuntime: harnessPolicy.runtime,
|
||||
config: cfg,
|
||||
}).map(normalizeProviderId);
|
||||
if (!profile || !acceptedAuthProviders.includes(profileProvider ?? "")) {
|
||||
await clearSessionAuthProfileOverride({
|
||||
|
||||
@@ -209,6 +209,22 @@ describe("models cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps --device-code to the provider device-code auth method", async () => {
|
||||
await runModelsCommand([
|
||||
"models",
|
||||
"auth",
|
||||
"login",
|
||||
"--provider",
|
||||
"openai-codex",
|
||||
"--device-code",
|
||||
]);
|
||||
|
||||
expectCommandOptions(modelsAuthLoginCommand, {
|
||||
provider: "openai-codex",
|
||||
method: "device-code",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes list-specific --agent and --json to models auth list", async () => {
|
||||
await runModelsCommand(["models", "auth", "list", "--agent", "poe", "--json"]);
|
||||
|
||||
|
||||
@@ -332,15 +332,21 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Run a provider plugin auth flow (OAuth/API key)")
|
||||
.option("--provider <id>", "Provider id registered by a plugin")
|
||||
.option("--method <id>", "Provider auth method id")
|
||||
.option("--device-code", "Use the provider device-code auth method", false)
|
||||
.option("--set-default", "Apply the provider's default model recommendation", false)
|
||||
.action(async (opts, command) => {
|
||||
if (opts.deviceCode && typeof opts.method === "string" && opts.method !== "device-code") {
|
||||
throw new Error(
|
||||
"--device-code cannot be combined with --method unless method is device-code.",
|
||||
);
|
||||
}
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command);
|
||||
const { modelsAuthLoginCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthLoginCommand(
|
||||
{
|
||||
provider: opts.provider as string | undefined,
|
||||
method: opts.method as string | undefined,
|
||||
method: opts.deviceCode ? "device-code" : (opts.method as string | undefined),
|
||||
setDefault: Boolean(opts.setDefault),
|
||||
agent,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ function resolveAuthProviderCandidates(params: {
|
||||
...listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider: params.provider,
|
||||
harnessRuntime: harnessPolicy.runtime,
|
||||
config: params.config,
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -794,6 +794,7 @@ async function prepareCronRunContext(params: {
|
||||
agentId,
|
||||
sessionKey: agentSessionKey,
|
||||
}).runtime,
|
||||
config: cfgWithAgentDefaults,
|
||||
}),
|
||||
agentDir,
|
||||
sessionEntry: cronSession.sessionEntry,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { resolveFastModeState } from "../agents/fast-mode.js";
|
||||
import { resolveModelAuthLabel } from "../agents/model-auth-label.js";
|
||||
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
|
||||
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../agents/openai-codex-routing.js";
|
||||
import {
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
@@ -147,7 +148,7 @@ async function resolveStatusHarnessId(params: {
|
||||
agentHarnessId: params.sessionEntry?.agentHarnessId,
|
||||
});
|
||||
const id = normalizeOptionalLowercaseString(selected.id);
|
||||
return id && id !== "pi" ? id : undefined;
|
||||
return id || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -234,15 +235,26 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
provider,
|
||||
effectiveHarness,
|
||||
});
|
||||
const selectedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider,
|
||||
harnessRuntime: effectiveHarness,
|
||||
config: cfg,
|
||||
});
|
||||
const activeProvider = modelRefs.active.provider || provider;
|
||||
const activeStatusProvider = resolveStatusRuntimeProvider({
|
||||
provider: activeProvider,
|
||||
effectiveHarness,
|
||||
});
|
||||
const activeAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({
|
||||
provider: activeProvider,
|
||||
harnessRuntime: effectiveHarness,
|
||||
config: cfg,
|
||||
});
|
||||
let selectedModelAuth = Object.hasOwn(params, "modelAuthOverride")
|
||||
? params.modelAuthOverride
|
||||
: resolveModelAuthLabel({
|
||||
provider: selectedStatusProvider,
|
||||
acceptedProviderIds: selectedAuthProviders,
|
||||
cfg,
|
||||
sessionEntry,
|
||||
agentDir: statusAgentDir,
|
||||
@@ -254,6 +266,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
: modelRefs.activeDiffers
|
||||
? resolveModelAuthLabel({
|
||||
provider: activeStatusProvider,
|
||||
acceptedProviderIds: activeAuthProviders,
|
||||
cfg,
|
||||
sessionEntry,
|
||||
agentDir: statusAgentDir,
|
||||
|
||||
Reference in New Issue
Block a user