mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-16 06:40:45 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
520
openclaw/extensions/openai/openai-provider.test.ts
Normal file
520
openclaw/extensions/openai/openai-provider.test.ts
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js";
|
||||
import { buildOpenAIProvider } from "./openai-provider.js";
|
||||
|
||||
const refreshOpenAICodexTokenMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./openai-codex-provider.runtime.js", () => ({
|
||||
refreshOpenAICodexToken: refreshOpenAICodexTokenMock,
|
||||
}));
|
||||
|
||||
function runWrappedPayloadCase(params: {
|
||||
wrap: NonNullable<ReturnType<typeof buildOpenAIProvider>["wrapStreamFn"]>;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
model:
|
||||
| Model<"openai-responses">
|
||||
| Model<"openai-codex-responses">
|
||||
| Model<"azure-openai-responses">;
|
||||
extraParams?: Record<string, unknown>;
|
||||
cfg?: Record<string, unknown>;
|
||||
payload?: Record<string, unknown>;
|
||||
}) {
|
||||
const payload = params.payload ?? { store: false };
|
||||
let capturedOptions: (SimpleStreamOptions & { openaiWsWarmup?: boolean }) | undefined;
|
||||
const baseStreamFn: StreamFn = (model, _context, options) => {
|
||||
capturedOptions = options as (SimpleStreamOptions & { openaiWsWarmup?: boolean }) | undefined;
|
||||
options?.onPayload?.(payload, model);
|
||||
return {} as ReturnType<StreamFn>;
|
||||
};
|
||||
|
||||
const streamFn = params.wrap({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
extraParams: params.extraParams,
|
||||
config: params.cfg as never,
|
||||
agentDir: "/tmp/openai-provider-test",
|
||||
streamFn: baseStreamFn,
|
||||
} as never);
|
||||
|
||||
const context: Context = { messages: [] };
|
||||
void streamFn?.(params.model, context, {});
|
||||
|
||||
return {
|
||||
payload,
|
||||
options: capturedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildOpenAIProvider", () => {
|
||||
it("resolves gpt-5.4 mini and nano from GPT-5 small-model templates", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const registry = {
|
||||
find(providerId: string, id: string) {
|
||||
if (providerId !== "openai") {
|
||||
return null;
|
||||
}
|
||||
if (id === "gpt-5-mini") {
|
||||
return {
|
||||
id,
|
||||
name: "GPT-5 mini",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
}
|
||||
if (id === "gpt-5-nano") {
|
||||
return {
|
||||
id,
|
||||
name: "GPT-5 nano",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0.5, output: 1, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 64_000,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const mini = provider.resolveDynamicModel?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4-mini",
|
||||
modelRegistry: registry as never,
|
||||
});
|
||||
const nano = provider.resolveDynamicModel?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4-nano",
|
||||
modelRegistry: registry as never,
|
||||
});
|
||||
|
||||
expect(mini).toMatchObject({
|
||||
provider: "openai",
|
||||
id: "gpt-5.4-mini",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
});
|
||||
expect(nano).toMatchObject({
|
||||
provider: "openai",
|
||||
id: "gpt-5.4-nano",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces gpt-5.4 mini and nano in xhigh and augmented catalog metadata", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
|
||||
expect(
|
||||
provider.supportsXHighThinking?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4-mini",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
expect(
|
||||
provider.supportsXHighThinking?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4-nano",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
|
||||
const entries = provider.augmentModelCatalog?.({
|
||||
env: process.env,
|
||||
entries: [
|
||||
{ provider: "openai", id: "gpt-5-mini", name: "GPT-5 mini" },
|
||||
{ provider: "openai", id: "gpt-5-nano", name: "GPT-5 nano" },
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
id: "gpt-5.4-mini",
|
||||
name: "gpt-5.4-mini",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 400_000,
|
||||
}),
|
||||
);
|
||||
expect(entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
id: "gpt-5.4-nano",
|
||||
name: "gpt-5.4-nano",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 400_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("owns native reasoning output mode for OpenAI and Azure OpenAI responses", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
|
||||
expect(
|
||||
provider.resolveReasoningOutputMode?.({
|
||||
provider: "openai",
|
||||
modelApi: "openai-responses",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toBe("native");
|
||||
expect(
|
||||
provider.resolveReasoningOutputMode?.({
|
||||
provider: "azure-openai-responses",
|
||||
modelApi: "azure-openai-responses",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toBe("native");
|
||||
});
|
||||
|
||||
it("keeps GPT-5.4 family metadata aligned with native OpenAI docs", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const codexProvider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
const openaiModel = provider.resolveDynamicModel?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
modelRegistry: { find: () => null },
|
||||
} as never);
|
||||
const codexModel = codexProvider.resolveDynamicModel?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
modelRegistry: { find: () => null },
|
||||
} as never);
|
||||
|
||||
expect(openaiModel).toMatchObject({
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
});
|
||||
expect(codexModel).toMatchObject({
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.4",
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps modern live selection on OpenAI 5.2+ and Codex 5.2+", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const codexProvider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(
|
||||
provider.isModernModelRef?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.0",
|
||||
} as never),
|
||||
).toBe(false);
|
||||
expect(
|
||||
provider.isModernModelRef?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.2",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
expect(
|
||||
provider.isModernModelRef?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
codexProvider.isModernModelRef?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.1-codex",
|
||||
} as never),
|
||||
).toBe(false);
|
||||
expect(
|
||||
codexProvider.isModernModelRef?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.1-codex-max",
|
||||
} as never),
|
||||
).toBe(false);
|
||||
expect(
|
||||
codexProvider.isModernModelRef?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.2-codex",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
expect(
|
||||
codexProvider.isModernModelRef?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("owns replay policy for OpenAI and Codex transports", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const codexProvider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(
|
||||
provider.buildReplayPolicy?.({
|
||||
provider: "openai",
|
||||
modelApi: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toEqual({
|
||||
sanitizeMode: "images-only",
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
sanitizeToolCallIds: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
provider.buildReplayPolicy?.({
|
||||
provider: "openai",
|
||||
modelApi: "openai-completions",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toEqual({
|
||||
sanitizeMode: "images-only",
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
codexProvider.buildReplayPolicy?.({
|
||||
provider: "openai-codex",
|
||||
modelApi: "openai-codex-responses",
|
||||
modelId: "gpt-5.4",
|
||||
} as never),
|
||||
).toEqual({
|
||||
sanitizeMode: "images-only",
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
sanitizeToolCallIds: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("owns direct OpenAI wrapper composition for responses payloads", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const wrap = provider.wrapStreamFn;
|
||||
expect(wrap).toBeTypeOf("function");
|
||||
if (!wrap) {
|
||||
throw new Error("expected OpenAI wrapper");
|
||||
}
|
||||
const extraParams = provider.prepareExtraParams?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
extraParams: {
|
||||
fastMode: true,
|
||||
serviceTier: "priority",
|
||||
textVerbosity: "low",
|
||||
},
|
||||
} as never);
|
||||
const result = runWrappedPayloadCase({
|
||||
wrap,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
extraParams: extraParams ?? undefined,
|
||||
model: {
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
} as Model<"openai-responses">,
|
||||
payload: {
|
||||
reasoning: { effort: "none" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(extraParams).toMatchObject({
|
||||
transport: "auto",
|
||||
openaiWsWarmup: true,
|
||||
});
|
||||
expect(result.payload.service_tier).toBe("priority");
|
||||
expect(result.payload.text).toEqual({ verbosity: "low" });
|
||||
expect(result.payload.reasoning).toEqual({ effort: "none" });
|
||||
});
|
||||
|
||||
it("preserves explicit OpenAI responses transport and warmup overrides", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
|
||||
const explicit = {
|
||||
transport: "websocket",
|
||||
openaiWsWarmup: false,
|
||||
fastMode: true,
|
||||
};
|
||||
|
||||
expect(
|
||||
provider.prepareExtraParams?.({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
extraParams: explicit,
|
||||
} as never),
|
||||
).toBe(explicit);
|
||||
});
|
||||
|
||||
it("defaults Codex responses transport without forcing warmup flags", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(
|
||||
provider.prepareExtraParams?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
extraParams: { effort: "high" },
|
||||
} as never),
|
||||
).toEqual({
|
||||
effort: "high",
|
||||
transport: "auto",
|
||||
});
|
||||
|
||||
const explicit = {
|
||||
transport: "sse",
|
||||
openaiWsWarmup: false,
|
||||
};
|
||||
expect(
|
||||
provider.prepareExtraParams?.({
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
extraParams: explicit,
|
||||
} as never),
|
||||
).toBe(explicit);
|
||||
});
|
||||
|
||||
it("shares OpenAI responses wrapper composition across provider variants", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const codexProvider = buildOpenAICodexProviderPlugin();
|
||||
|
||||
expect(provider.wrapStreamFn).toBe(codexProvider.wrapStreamFn);
|
||||
expect(provider.buildReplayPolicy).toBe(codexProvider.buildReplayPolicy);
|
||||
expect(provider.resolveTransportTurnState).toBe(codexProvider.resolveTransportTurnState);
|
||||
expect(provider.resolveWebSocketSessionPolicy).toBe(
|
||||
codexProvider.resolveWebSocketSessionPolicy,
|
||||
);
|
||||
});
|
||||
|
||||
it("owns Azure OpenAI reasoning compatibility without forcing OpenAI transport defaults", () => {
|
||||
const provider = buildOpenAIProvider();
|
||||
const wrap = provider.wrapStreamFn;
|
||||
expect(wrap).toBeTypeOf("function");
|
||||
if (!wrap) {
|
||||
throw new Error("expected Azure OpenAI wrapper");
|
||||
}
|
||||
const result = runWrappedPayloadCase({
|
||||
wrap,
|
||||
provider: "azure-openai-responses",
|
||||
modelId: "gpt-5.4",
|
||||
model: {
|
||||
api: "azure-openai-responses",
|
||||
provider: "azure-openai-responses",
|
||||
id: "gpt-5.4",
|
||||
baseUrl: "https://example.openai.azure.com/openai/v1",
|
||||
} as Model<"azure-openai-responses">,
|
||||
payload: {
|
||||
reasoning: { effort: "none" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.options?.transport).toBeUndefined();
|
||||
expect(result.options?.openaiWsWarmup).toBeUndefined();
|
||||
expect(result.payload.reasoning).toEqual({ effort: "none" });
|
||||
});
|
||||
|
||||
it("owns Codex wrapper composition for responses payloads", () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const wrap = provider.wrapStreamFn;
|
||||
expect(wrap).toBeTypeOf("function");
|
||||
if (!wrap) {
|
||||
throw new Error("expected Codex wrapper");
|
||||
}
|
||||
const result = runWrappedPayloadCase({
|
||||
wrap,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
extraParams: {
|
||||
fastMode: true,
|
||||
serviceTier: "priority",
|
||||
text_verbosity: "high",
|
||||
},
|
||||
cfg: {
|
||||
auth: {
|
||||
profiles: {
|
||||
"openai-codex:default": {
|
||||
provider: "openai-codex",
|
||||
mode: "oauth",
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: true,
|
||||
openaiCodex: {
|
||||
enabled: true,
|
||||
mode: "live",
|
||||
allowedDomains: ["example.com"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
model: {
|
||||
api: "openai-codex-responses",
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.4",
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
} as Model<"openai-codex-responses">,
|
||||
payload: {
|
||||
store: false,
|
||||
text: { verbosity: "medium" },
|
||||
tools: [{ type: "function", name: "read" }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.payload.store).toBe(false);
|
||||
expect(result.payload.service_tier).toBe("priority");
|
||||
expect(result.payload.text).toEqual({ verbosity: "high" });
|
||||
expect(result.payload.tools).toEqual([
|
||||
{ type: "function", name: "read" },
|
||||
{
|
||||
type: "web_search",
|
||||
external_web_access: true,
|
||||
filters: { allowed_domains: ["example.com"] },
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("falls back to cached codex oauth credentials on accountId extraction failures", async () => {
|
||||
const provider = buildOpenAICodexProviderPlugin();
|
||||
const credential = {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: "cached-access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
};
|
||||
|
||||
refreshOpenAICodexTokenMock.mockReset();
|
||||
refreshOpenAICodexTokenMock.mockRejectedValueOnce(
|
||||
new Error("Failed to extract accountId from token"),
|
||||
);
|
||||
|
||||
await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(credential);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue