mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
fix: route codex responses over websocket and preserve tool warnings (#53702) (thanks @Nanako0129)
* fix: route codex responses over websocket and suppress gated core tool warnings * fix: rebase codex websocket patch onto main * fix: preserve explicit alsoAllow warnings (#53702) (thanks @Nanako0129) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -425,7 +425,7 @@ let runEmbeddedAttemptPromise:
|
||||
| Promise<typeof import("./attempt.js").runEmbeddedAttempt>
|
||||
| undefined;
|
||||
|
||||
export async function loadRunEmbeddedAttempt() {
|
||||
async function loadRunEmbeddedAttempt() {
|
||||
runEmbeddedAttemptPromise ??= import("./attempt.js").then((mod) => mod.runEmbeddedAttempt);
|
||||
return await runEmbeddedAttemptPromise;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldUseOpenAIWebSocketTransport } from "./attempt.thread-helpers.js";
|
||||
|
||||
describe("openai websocket transport selection", () => {
|
||||
it("accepts the direct OpenAI responses transport pair", () => {
|
||||
expect(
|
||||
shouldUseOpenAIWebSocketTransport({
|
||||
provider: "openai",
|
||||
modelApi: "openai-responses",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts the Codex responses transport pair", () => {
|
||||
expect(
|
||||
shouldUseOpenAIWebSocketTransport({
|
||||
provider: "openai-codex",
|
||||
modelApi: "openai-codex-responses",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects mismatched OpenAI websocket transport pairs", () => {
|
||||
expect(
|
||||
shouldUseOpenAIWebSocketTransport({
|
||||
provider: "openai",
|
||||
modelApi: "openai-codex-responses",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldUseOpenAIWebSocketTransport({
|
||||
provider: "openai-codex",
|
||||
modelApi: "openai-responses",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldUseOpenAIWebSocketTransport({
|
||||
provider: "anthropic",
|
||||
modelApi: "openai-responses",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,16 @@ export function resolveAttemptSpawnWorkspaceDir(params: {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function shouldUseOpenAIWebSocketTransport(params: {
|
||||
provider: string;
|
||||
modelApi?: string | null;
|
||||
}): boolean {
|
||||
return (
|
||||
(params.modelApi === "openai-responses" && params.provider === "openai") ||
|
||||
(params.modelApi === "openai-codex-responses" && params.provider === "openai-codex")
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldAppendAttemptCacheTtl(params: {
|
||||
timedOutDuringCompaction: boolean;
|
||||
compactionOccurredThisAttempt: boolean;
|
||||
|
||||
@@ -148,6 +148,7 @@ import {
|
||||
appendAttemptCacheTtlIfNeeded,
|
||||
composeSystemPromptWithHookContext,
|
||||
resolveAttemptSpawnWorkspaceDir,
|
||||
shouldUseOpenAIWebSocketTransport,
|
||||
} from "./attempt.thread-helpers.js";
|
||||
import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js";
|
||||
import {
|
||||
@@ -2247,7 +2248,12 @@ export async function runEmbeddedAttempt(
|
||||
});
|
||||
activeSession.agent.streamFn = ollamaStreamFn;
|
||||
ensureCustomApiRegistered(params.model.api, ollamaStreamFn);
|
||||
} else if (params.model.api === "openai-responses" && params.provider === "openai") {
|
||||
} else if (
|
||||
shouldUseOpenAIWebSocketTransport({
|
||||
provider: params.provider,
|
||||
modelApi: params.model.api,
|
||||
})
|
||||
) {
|
||||
const wsApiKey = await params.authStorage.getApiKey(params.provider);
|
||||
if (wsApiKey) {
|
||||
activeSession.agent.streamFn = createOpenAIWebSocketStreamFn(wsApiKey, params.sessionId, {
|
||||
|
||||
@@ -589,8 +589,10 @@ export function createOpenClawCodingTools(options?: {
|
||||
...buildDefaultToolPolicyPipelineSteps({
|
||||
profilePolicy: profilePolicyWithAlsoAllow,
|
||||
profile,
|
||||
profileAlsoAllow,
|
||||
providerProfilePolicy: providerProfilePolicyWithAlsoAllow,
|
||||
providerProfile,
|
||||
providerProfileAlsoAllow,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("tool-policy-pipeline", () => {
|
||||
expect(warnings[0]).toContain("unknown entries (wat)");
|
||||
});
|
||||
|
||||
test("warns gated core tools as unavailable instead of plugin-only unknowns", () => {
|
||||
test("suppresses built-in profile warnings for unavailable gated core tools", () => {
|
||||
const warnings: string[] = [];
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
applyToolPolicyPipeline({
|
||||
@@ -66,6 +66,52 @@ describe("tool-policy-pipeline", () => {
|
||||
policy: { allow: ["apply_patch"] },
|
||||
label: "tools.profile (coding)",
|
||||
stripPluginOnlyAllowlist: true,
|
||||
suppressUnavailableCoreToolWarning: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
test("still warns for profile steps when explicit alsoAllow entries are present", () => {
|
||||
const warnings: string[] = [];
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
applyToolPolicyPipeline({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
tools: tools as any,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
steps: [
|
||||
{
|
||||
policy: { allow: ["apply_patch"] },
|
||||
label: "tools.profile (coding)",
|
||||
stripPluginOnlyAllowlist: true,
|
||||
suppressUnavailableCoreToolWarning: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(warnings.length).toBe(1);
|
||||
expect(warnings[0]).toContain("unknown entries (apply_patch)");
|
||||
expect(warnings[0]).toContain(
|
||||
"shipped core tools but unavailable in the current runtime/provider/model/config",
|
||||
);
|
||||
});
|
||||
|
||||
test("still warns for explicit allowlists that mention unavailable gated core tools", () => {
|
||||
const warnings: string[] = [];
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
applyToolPolicyPipeline({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
tools: tools as any,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
steps: [
|
||||
{
|
||||
policy: { allow: ["apply_patch"] },
|
||||
label: "tools.allow",
|
||||
stripPluginOnlyAllowlist: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -88,8 +134,8 @@ describe("tool-policy-pipeline", () => {
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
{
|
||||
policy: { allow: ["apply_patch"] },
|
||||
label: "tools.profile (coding)",
|
||||
policy: { allow: ["wat"] },
|
||||
label: "tools.allow",
|
||||
stripPluginOnlyAllowlist: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -30,13 +30,16 @@ export type ToolPolicyPipelineStep = {
|
||||
policy: ToolPolicyLike | undefined;
|
||||
label: string;
|
||||
stripPluginOnlyAllowlist?: boolean;
|
||||
suppressUnavailableCoreToolWarning?: boolean;
|
||||
};
|
||||
|
||||
export function buildDefaultToolPolicyPipelineSteps(params: {
|
||||
profilePolicy?: ToolPolicyLike;
|
||||
profile?: string;
|
||||
profileAlsoAllow?: string[];
|
||||
providerProfilePolicy?: ToolPolicyLike;
|
||||
providerProfile?: string;
|
||||
providerProfileAlsoAllow?: string[];
|
||||
globalPolicy?: ToolPolicyLike;
|
||||
globalProviderPolicy?: ToolPolicyLike;
|
||||
agentPolicy?: ToolPolicyLike;
|
||||
@@ -52,6 +55,8 @@ export function buildDefaultToolPolicyPipelineSteps(params: {
|
||||
policy: params.profilePolicy,
|
||||
label: profile ? `tools.profile (${profile})` : "tools.profile",
|
||||
stripPluginOnlyAllowlist: true,
|
||||
suppressUnavailableCoreToolWarning:
|
||||
!Array.isArray(params.profileAlsoAllow) || params.profileAlsoAllow.length === 0,
|
||||
},
|
||||
{
|
||||
policy: params.providerProfilePolicy,
|
||||
@@ -59,6 +64,9 @@ export function buildDefaultToolPolicyPipelineSteps(params: {
|
||||
? `tools.byProvider.profile (${providerProfile})`
|
||||
: "tools.byProvider.profile",
|
||||
stripPluginOnlyAllowlist: true,
|
||||
suppressUnavailableCoreToolWarning:
|
||||
!Array.isArray(params.providerProfileAlsoAllow) ||
|
||||
params.providerProfileAlsoAllow.length === 0,
|
||||
},
|
||||
{ policy: params.globalPolicy, label: "tools.allow", stripPluginOnlyAllowlist: true },
|
||||
{
|
||||
@@ -113,14 +121,22 @@ export function applyToolPolicyPipeline(params: {
|
||||
isKnownCoreToolId(entry),
|
||||
);
|
||||
const otherEntries = resolved.unknownAllowlist.filter((entry) => !isKnownCoreToolId(entry));
|
||||
const suffix = describeUnknownAllowlistSuffix({
|
||||
strippedAllowlist: resolved.strippedAllowlist,
|
||||
hasGatedCoreEntries: gatedCoreEntries.length > 0,
|
||||
hasOtherEntries: otherEntries.length > 0,
|
||||
});
|
||||
const warning = `tools: ${step.label} allowlist contains unknown entries (${entries}). ${suffix}`;
|
||||
if (rememberToolPolicyWarning(warning)) {
|
||||
params.warn(warning);
|
||||
if (
|
||||
!shouldSuppressUnavailableCoreToolWarning({
|
||||
suppressUnavailableCoreToolWarning: step.suppressUnavailableCoreToolWarning === true,
|
||||
hasGatedCoreEntries: gatedCoreEntries.length > 0,
|
||||
hasOtherEntries: otherEntries.length > 0,
|
||||
})
|
||||
) {
|
||||
const suffix = describeUnknownAllowlistSuffix({
|
||||
strippedAllowlist: resolved.strippedAllowlist,
|
||||
hasGatedCoreEntries: gatedCoreEntries.length > 0,
|
||||
hasOtherEntries: otherEntries.length > 0,
|
||||
});
|
||||
const warning = `tools: ${step.label} allowlist contains unknown entries (${entries}). ${suffix}`;
|
||||
if (rememberToolPolicyWarning(warning)) {
|
||||
params.warn(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
policy = resolved.policy;
|
||||
@@ -132,6 +148,21 @@ export function applyToolPolicyPipeline(params: {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function shouldSuppressUnavailableCoreToolWarning(params: {
|
||||
suppressUnavailableCoreToolWarning: boolean;
|
||||
hasGatedCoreEntries: boolean;
|
||||
hasOtherEntries: boolean;
|
||||
}): boolean {
|
||||
if (
|
||||
!params.suppressUnavailableCoreToolWarning ||
|
||||
!params.hasGatedCoreEntries ||
|
||||
params.hasOtherEntries
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function describeUnknownAllowlistSuffix(params: {
|
||||
strippedAllowlist: boolean;
|
||||
hasGatedCoreEntries: boolean;
|
||||
|
||||
@@ -278,8 +278,10 @@ export async function handleToolsInvokeHttpRequest(
|
||||
...buildDefaultToolPolicyPipelineSteps({
|
||||
profilePolicy: profilePolicyWithAlsoAllow,
|
||||
profile,
|
||||
profileAlsoAllow,
|
||||
providerProfilePolicy: providerProfilePolicyWithAlsoAllow,
|
||||
providerProfile,
|
||||
providerProfileAlsoAllow,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
|
||||
Reference in New Issue
Block a user