Compare commits
3 Commits
main
...
codex/mode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee383594e1 | ||
|
|
ca73c86a4e | ||
|
|
02d749ae36 |
@ -153,6 +153,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
|
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
|
||||||
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
|
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
|
||||||
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
|
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
|
||||||
|
- Models/CLI: keep `models list --all` aligned with synthetic catalog rows so forward-compat entries like `openai-codex/gpt-5.4` stay visible without re-auth. (#40160) Thanks @dorukardahan.
|
||||||
- Cron/announce delivery robustness: bypass pending-descendant announce guards for cron completion sends, ensure named-agent announce routes have outbound session entries, and fall back to direct delivery only when an announce send was actually attempted and failed. (from #35185, #32443, #34987) Thanks @Sid-Qin, @scoootscooob, and @bmendonca3.
|
- Cron/announce delivery robustness: bypass pending-descendant announce guards for cron completion sends, ensure named-agent announce routes have outbound session entries, and fall back to direct delivery only when an announce send was actually attempted and failed. (from #35185, #32443, #34987) Thanks @Sid-Qin, @scoootscooob, and @bmendonca3.
|
||||||
- Cron/announce best-effort fallback: run direct outbound fallback after attempted announce failures even when delivery is configured as best-effort, so Telegram cron sends are not left as attempted-but-undelivered after `cron announce delivery failed` warnings.
|
- Cron/announce best-effort fallback: run direct outbound fallback after attempted announce failures even when delivery is configured as best-effort, so Telegram cron sends are not left as attempted-but-undelivered after `cron announce delivery failed` warnings.
|
||||||
- Auto-reply/system events: restore runtime system events to the message timeline (`System:` lines), preserve think-hint parsing with prepended events, and carry events into deferred followup/collect/steer-backlog prompts to keep cache behavior stable without dropping queued metadata. (#34794) Thanks @anisoptera.
|
- Auto-reply/system events: restore runtime system events to the message timeline (`System:` lines), preserve think-hint parsing with prepended events, and carry events into deferred followup/collect/steer-backlog prompts to keep cache behavior stable without dropping queued metadata. (#34794) Thanks @anisoptera.
|
||||||
|
|||||||
@ -354,8 +354,8 @@ describe("models list/status", () => {
|
|||||||
|
|
||||||
await modelsListCommand({ all: true, json: true }, runtime);
|
await modelsListCommand({ all: true, json: true }, runtime);
|
||||||
|
|
||||||
expect(ensureOpenClawModelsJson).toHaveBeenCalledTimes(1);
|
expect(ensureOpenClawModelsJson).toHaveBeenCalled();
|
||||||
expect(ensureOpenClawModelsJson).toHaveBeenCalledWith(resolvedConfig);
|
expect(ensureOpenClawModelsJson.mock.calls[0]?.[0]).toEqual(resolvedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toModelRow does not crash without cfg/authStore when availability is undefined", async () => {
|
it("toModelRow does not crash without cfg/authStore when availability is undefined", async () => {
|
||||||
|
|||||||
@ -38,6 +38,7 @@ const mocks = vi.hoisted(() => {
|
|||||||
loadModelRegistry: vi
|
loadModelRegistry: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ models: [], availableKeys: new Set(), registry: {} }),
|
.mockResolvedValue({ models: [], availableKeys: new Set(), registry: {} }),
|
||||||
|
loadModelCatalog: vi.fn().mockResolvedValue([]),
|
||||||
resolveConfiguredEntries: vi.fn().mockReturnValue({
|
resolveConfiguredEntries: vi.fn().mockReturnValue({
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
@ -66,6 +67,8 @@ const mocks = vi.hoisted(() => {
|
|||||||
|
|
||||||
vi.mock("../../config/config.js", () => ({
|
vi.mock("../../config/config.js", () => ({
|
||||||
loadConfig: mocks.loadConfig,
|
loadConfig: mocks.loadConfig,
|
||||||
|
getRuntimeConfigSnapshot: vi.fn().mockReturnValue(null),
|
||||||
|
getRuntimeConfigSourceSnapshot: vi.fn().mockReturnValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../agents/auth-profiles.js", async (importOriginal) => {
|
vi.mock("../../agents/auth-profiles.js", async (importOriginal) => {
|
||||||
@ -77,6 +80,10 @@ vi.mock("../../agents/auth-profiles.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("../../agents/model-catalog.js", () => ({
|
||||||
|
loadModelCatalog: mocks.loadModelCatalog,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("./list.registry.js", async (importOriginal) => {
|
vi.mock("./list.registry.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("./list.registry.js")>();
|
const actual = await importOriginal<typeof import("./list.registry.js")>();
|
||||||
return {
|
return {
|
||||||
@ -177,25 +184,163 @@ describe("modelsListCommand forward-compat", () => {
|
|||||||
availableKeys: new Set(),
|
availableKeys: new Set(),
|
||||||
registry: {},
|
registry: {},
|
||||||
});
|
});
|
||||||
mocks.listProfilesForProvider.mockImplementationOnce((_: unknown, provider: string) =>
|
mocks.listProfilesForProvider.mockImplementation((_: unknown, provider: string) =>
|
||||||
provider === "openai-codex" ? ([{ id: "profile-1" }] as Array<Record<string, unknown>>) : [],
|
provider === "openai-codex" ? ([{ id: "profile-1" }] as Array<Record<string, unknown>>) : [],
|
||||||
);
|
);
|
||||||
const runtime = { log: vi.fn(), error: vi.fn() };
|
const runtime = { log: vi.fn(), error: vi.fn() };
|
||||||
|
|
||||||
await modelsListCommand({ json: true }, runtime as never);
|
try {
|
||||||
|
await modelsListCommand({ json: true }, runtime as never);
|
||||||
|
|
||||||
|
expect(mocks.printModelTable).toHaveBeenCalled();
|
||||||
|
const rows = mocks.printModelTable.mock.calls.at(-1)?.[0] as Array<{
|
||||||
|
key: string;
|
||||||
|
available: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
expect(rows).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
key: "openai-codex/gpt-5.4",
|
||||||
|
available: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
mocks.listProfilesForProvider.mockReturnValue([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes synthetic codex gpt-5.4 in --all output when catalog supports it", async () => {
|
||||||
|
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||||
|
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
provider: "openai-codex",
|
||||||
|
id: "gpt-5.3-codex",
|
||||||
|
name: "GPT-5.3 Codex",
|
||||||
|
api: "openai-codex-responses",
|
||||||
|
baseUrl: "https://chatgpt.com/backend-api",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 272000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableKeys: new Set(["openai-codex/gpt-5.3-codex"]),
|
||||||
|
registry: {},
|
||||||
|
});
|
||||||
|
mocks.loadModelCatalog.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
provider: "openai-codex",
|
||||||
|
id: "gpt-5.3-codex",
|
||||||
|
name: "GPT-5.3 Codex",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 272000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: "openai-codex",
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "GPT-5.4",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 272000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
mocks.listProfilesForProvider.mockImplementation((_: unknown, provider: string) =>
|
||||||
|
provider === "openai-codex" ? ([{ id: "profile-1" }] as Array<Record<string, unknown>>) : [],
|
||||||
|
);
|
||||||
|
mocks.resolveModelWithRegistry.mockImplementation(
|
||||||
|
({ provider, modelId }: { provider: string; modelId: string }) => {
|
||||||
|
if (provider !== "openai-codex") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (modelId === "gpt-5.3-codex") {
|
||||||
|
return {
|
||||||
|
provider: "openai-codex",
|
||||||
|
id: "gpt-5.3-codex",
|
||||||
|
name: "GPT-5.3 Codex",
|
||||||
|
api: "openai-codex-responses",
|
||||||
|
baseUrl: "https://chatgpt.com/backend-api",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 272000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (modelId === "gpt-5.4") {
|
||||||
|
return {
|
||||||
|
provider: "openai-codex",
|
||||||
|
id: "gpt-5.4",
|
||||||
|
name: "GPT-5.4",
|
||||||
|
api: "openai-codex-responses",
|
||||||
|
baseUrl: "https://chatgpt.com/backend-api",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 272000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const runtime = { log: vi.fn(), error: vi.fn() };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await modelsListCommand(
|
||||||
|
{ all: true, provider: "openai-codex", json: true },
|
||||||
|
runtime as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mocks.printModelTable).toHaveBeenCalled();
|
||||||
|
const rows = mocks.printModelTable.mock.calls.at(-1)?.[0] as Array<{
|
||||||
|
key: string;
|
||||||
|
available: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
expect(rows).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: "openai-codex/gpt-5.3-codex",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
key: "openai-codex/gpt-5.4",
|
||||||
|
available: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
mocks.listProfilesForProvider.mockReturnValue([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps discovered rows in --all output when catalog lookup is empty", async () => {
|
||||||
|
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||||
|
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
provider: "openai-codex",
|
||||||
|
id: "gpt-5.3-codex",
|
||||||
|
name: "GPT-5.3 Codex",
|
||||||
|
api: "openai-codex-responses",
|
||||||
|
baseUrl: "https://chatgpt.com/backend-api",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 272000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableKeys: new Set(["openai-codex/gpt-5.3-codex"]),
|
||||||
|
registry: {},
|
||||||
|
});
|
||||||
|
mocks.loadModelCatalog.mockResolvedValueOnce([]);
|
||||||
|
const runtime = { log: vi.fn(), error: vi.fn() };
|
||||||
|
|
||||||
|
await modelsListCommand({ all: true, provider: "openai-codex", json: true }, runtime as never);
|
||||||
|
|
||||||
expect(mocks.printModelTable).toHaveBeenCalled();
|
expect(mocks.printModelTable).toHaveBeenCalled();
|
||||||
const rows = mocks.printModelTable.mock.calls.at(-1)?.[0] as Array<{
|
const rows = mocks.printModelTable.mock.calls.at(-1)?.[0] as Array<{ key: string }>;
|
||||||
key: string;
|
|
||||||
available: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
expect(rows).toContainEqual(
|
expect(rows).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
key: "openai-codex/gpt-5.4",
|
key: "openai-codex/gpt-5.3-codex",
|
||||||
available: true,
|
|
||||||
}),
|
}),
|
||||||
);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exits with an error when configured-mode listing has no model registry", async () => {
|
it("exits with an error when configured-mode listing has no model registry", async () => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||||
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
||||||
import { parseModelRef } from "../../agents/model-selection.js";
|
import { parseModelRef } from "../../agents/model-selection.js";
|
||||||
import { resolveModelWithRegistry } from "../../agents/pi-embedded-runner/model.js";
|
import { resolveModelWithRegistry } from "../../agents/pi-embedded-runner/model.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
@ -69,6 +70,7 @@ export async function modelsListCommand(
|
|||||||
const rows: ModelRow[] = [];
|
const rows: ModelRow[] = [];
|
||||||
|
|
||||||
if (opts.all) {
|
if (opts.all) {
|
||||||
|
const seenKeys = new Set<string>();
|
||||||
const sorted = [...models].toSorted((a, b) => {
|
const sorted = [...models].toSorted((a, b) => {
|
||||||
const p = a.provider.localeCompare(b.provider);
|
const p = a.provider.localeCompare(b.provider);
|
||||||
if (p !== 0) {
|
if (p !== 0) {
|
||||||
@ -97,6 +99,46 @@ export async function modelsListCommand(
|
|||||||
authStore,
|
authStore,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
seenKeys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelRegistry) {
|
||||||
|
const catalog = await loadModelCatalog({ config: cfg });
|
||||||
|
for (const entry of catalog) {
|
||||||
|
if (providerFilter && entry.provider.toLowerCase() !== providerFilter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = modelKey(entry.provider, entry.id);
|
||||||
|
if (seenKeys.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const model = resolveModelWithRegistry({
|
||||||
|
provider: entry.provider,
|
||||||
|
modelId: entry.id,
|
||||||
|
modelRegistry,
|
||||||
|
cfg,
|
||||||
|
});
|
||||||
|
if (!model) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (opts.local && !isLocalBaseUrl(model.baseUrl)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const configured = configuredByKey.get(key);
|
||||||
|
rows.push(
|
||||||
|
toModelRow({
|
||||||
|
model,
|
||||||
|
key,
|
||||||
|
tags: configured ? Array.from(configured.tags) : [],
|
||||||
|
aliases: configured?.aliases ?? [],
|
||||||
|
availableKeys,
|
||||||
|
cfg,
|
||||||
|
authStore,
|
||||||
|
allowProviderAvailabilityFallback: !discoveredKeys.has(key),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
seenKeys.add(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const registry = modelRegistry;
|
const registry = modelRegistry;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user