mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 23:55:12 +00:00
fix(codex): honor OAuth contextTokens in native harness
Fixes #77858. Co-authored-by: Edionwheels <267595845+lilesjtu@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -143,6 +143,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/media: avoid direct generated-media completion fallback while the announce-agent run is still pending, so async video and music completions do not duplicate raw media messages. (#77754)
|
||||
- WebChat/Codex media: stage Codex app-server generated local images into managed media before Gateway display, so Codex-home image paths no longer hit `LocalMediaAccessError` while keeping Codex home out of the display allowlist. Thanks @frankekn.
|
||||
- Plugins/update: repair plugin-local `openclaw` peer links for all recorded npm plugins after any npm update mutates the shared managed npm tree, so targeted or batch updates cannot leave Codex, Discord, or Brave with pruned SDK imports. (#77787) Thanks @ProspectOre.
|
||||
- Codex harness: honor `models.providers.openai-codex.models[].contextTokens` for native `openai/*` Codex runtime runs and `/status` context reporting, so subscription-backed Codex agents use the configured OAuth context cap without inflating past the runtime model window. Fixes #77858. Thanks @lilesjtu.
|
||||
- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc.
|
||||
- Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd.
|
||||
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd.
|
||||
|
||||
@@ -35,6 +35,7 @@ type DiscoveredModel = {
|
||||
name?: string;
|
||||
provider: string;
|
||||
contextWindow?: number;
|
||||
contextTokens?: number;
|
||||
reasoning?: boolean;
|
||||
input?: ModelInputType[];
|
||||
compat?: ModelCatalogEntry["compat"];
|
||||
@@ -161,6 +162,9 @@ export function loadManifestModelCatalog(params: {
|
||||
if (contextWindow) {
|
||||
entry.contextWindow = contextWindow;
|
||||
}
|
||||
if (row.contextTokens) {
|
||||
entry.contextTokens = row.contextTokens;
|
||||
}
|
||||
if (typeof row.reasoning === "boolean") {
|
||||
entry.reasoning = row.reasoning;
|
||||
}
|
||||
@@ -189,6 +193,7 @@ function normalizePersistedModelCatalogEntry(
|
||||
entry: Record<string, unknown>,
|
||||
defaults?: {
|
||||
contextWindow?: number;
|
||||
contextTokens?: number;
|
||||
},
|
||||
): ModelCatalogEntry | undefined {
|
||||
const id = normalizeOptionalString(entry.id) ?? "";
|
||||
@@ -206,6 +211,12 @@ function normalizePersistedModelCatalogEntry(
|
||||
: defaults?.contextWindow !== undefined
|
||||
? defaults.contextWindow
|
||||
: PI_CUSTOM_MODEL_DEFAULT_CONTEXT_WINDOW;
|
||||
const contextTokens =
|
||||
typeof entry?.contextTokens === "number" && entry.contextTokens > 0
|
||||
? entry.contextTokens
|
||||
: defaults?.contextTokens !== undefined
|
||||
? defaults.contextTokens
|
||||
: undefined;
|
||||
const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : false;
|
||||
const parsedInput = Array.isArray(entry?.input)
|
||||
? entry.input.filter((value): value is ModelInputType =>
|
||||
@@ -217,7 +228,16 @@ function normalizePersistedModelCatalogEntry(
|
||||
entry?.compat && typeof entry.compat === "object"
|
||||
? (entry.compat as ModelCatalogEntry["compat"])
|
||||
: undefined;
|
||||
return { id, name, provider, contextWindow, reasoning, input, compat };
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
provider,
|
||||
contextWindow,
|
||||
...(contextTokens !== undefined ? { contextTokens } : {}),
|
||||
reasoning,
|
||||
input,
|
||||
compat,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadReadOnlyPersistedModelCatalog(params?: {
|
||||
@@ -242,9 +262,14 @@ async function loadReadOnlyPersistedModelCatalog(params?: {
|
||||
typeof providerConfig?.contextWindow === "number" && providerConfig.contextWindow > 0
|
||||
? providerConfig.contextWindow
|
||||
: undefined;
|
||||
const providerContextTokens =
|
||||
typeof providerConfig?.contextTokens === "number" && providerConfig.contextTokens > 0
|
||||
? providerConfig.contextTokens
|
||||
: undefined;
|
||||
for (const entry of providerConfig.models as Record<string, unknown>[]) {
|
||||
const normalized = normalizePersistedModelCatalogEntry(providerRaw, entry, {
|
||||
contextWindow: providerContextWindow,
|
||||
contextTokens: providerContextTokens,
|
||||
});
|
||||
if (normalized && !shouldSuppressBuiltInModel(normalized)) {
|
||||
models.push(normalized);
|
||||
@@ -370,10 +395,23 @@ export async function loadModelCatalog(params?: {
|
||||
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
||||
? entry.contextWindow
|
||||
: undefined;
|
||||
const contextTokens =
|
||||
typeof entry?.contextTokens === "number" && entry.contextTokens > 0
|
||||
? entry.contextTokens
|
||||
: undefined;
|
||||
const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined;
|
||||
const input = Array.isArray(entry?.input) ? entry.input : undefined;
|
||||
const compat = entry?.compat && typeof entry.compat === "object" ? entry.compat : undefined;
|
||||
models.push({ id, name, provider, contextWindow, reasoning, input, compat });
|
||||
models.push({
|
||||
id,
|
||||
name,
|
||||
provider,
|
||||
contextWindow,
|
||||
...(contextTokens !== undefined ? { contextTokens } : {}),
|
||||
reasoning,
|
||||
input,
|
||||
compat,
|
||||
});
|
||||
}
|
||||
if (!readOnly) {
|
||||
const supplemental = await augmentModelCatalogWithProviderPlugins({
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ModelCatalogEntry = {
|
||||
provider: string;
|
||||
alias?: string;
|
||||
contextWindow?: number;
|
||||
contextTokens?: number;
|
||||
reasoning?: boolean;
|
||||
input?: ModelInputType[];
|
||||
compat?: ModelCompatConfig;
|
||||
|
||||
@@ -442,6 +442,7 @@ function applyModelCatalogMetadata(params: {
|
||||
}
|
||||
const nextAlias = alias ?? params.entry.alias;
|
||||
const nextContextWindow = configuredEntry?.contextWindow ?? params.entry.contextWindow;
|
||||
const nextContextTokens = configuredEntry?.contextTokens ?? params.entry.contextTokens;
|
||||
const nextReasoning = configuredEntry?.reasoning ?? params.entry.reasoning;
|
||||
const nextInput = configuredEntry?.input ?? params.entry.input;
|
||||
const nextCompat = configuredEntry?.compat ?? params.entry.compat;
|
||||
@@ -451,6 +452,7 @@ function applyModelCatalogMetadata(params: {
|
||||
name: configuredEntry?.name ?? params.entry.name,
|
||||
...(nextAlias ? { alias: nextAlias } : {}),
|
||||
...(nextContextWindow !== undefined ? { contextWindow: nextContextWindow } : {}),
|
||||
...(nextContextTokens !== undefined ? { contextTokens: nextContextTokens } : {}),
|
||||
...(nextReasoning !== undefined ? { reasoning: nextReasoning } : {}),
|
||||
...(nextInput ? { input: nextInput } : {}),
|
||||
...(nextCompat ? { compat: nextCompat } : {}),
|
||||
@@ -465,6 +467,7 @@ function buildSyntheticAllowedCatalogEntry(params: {
|
||||
const configuredEntry = params.metadata.configuredByKey.get(key);
|
||||
const alias = params.metadata.aliasByKey.get(key);
|
||||
const nextContextWindow = configuredEntry?.contextWindow;
|
||||
const nextContextTokens = configuredEntry?.contextTokens;
|
||||
const nextReasoning = configuredEntry?.reasoning;
|
||||
const nextInput = configuredEntry?.input;
|
||||
const nextCompat = configuredEntry?.compat;
|
||||
@@ -475,6 +478,7 @@ function buildSyntheticAllowedCatalogEntry(params: {
|
||||
provider: params.parsed.provider,
|
||||
...(alias ? { alias } : {}),
|
||||
...(nextContextWindow !== undefined ? { contextWindow: nextContextWindow } : {}),
|
||||
...(nextContextTokens !== undefined ? { contextTokens: nextContextTokens } : {}),
|
||||
...(nextReasoning !== undefined ? { reasoning: nextReasoning } : {}),
|
||||
...(nextInput ? { input: nextInput } : {}),
|
||||
...(nextCompat ? { compat: nextCompat } : {}),
|
||||
@@ -836,6 +840,10 @@ export function buildConfiguredModelCatalog(params: { cfg: OpenClawConfig }): Mo
|
||||
typeof model?.contextWindow === "number" && model.contextWindow > 0
|
||||
? model.contextWindow
|
||||
: undefined;
|
||||
const contextTokens =
|
||||
typeof model?.contextTokens === "number" && model.contextTokens > 0
|
||||
? model.contextTokens
|
||||
: undefined;
|
||||
const reasoning = typeof model?.reasoning === "boolean" ? model.reasoning : undefined;
|
||||
const input = Array.isArray(model?.input) ? model.input : undefined;
|
||||
const compat = model?.compat && typeof model.compat === "object" ? model.compat : undefined;
|
||||
@@ -844,6 +852,7 @@ export function buildConfiguredModelCatalog(params: { cfg: OpenClawConfig }): Mo
|
||||
id,
|
||||
name,
|
||||
contextWindow,
|
||||
contextTokens,
|
||||
reasoning,
|
||||
input,
|
||||
compat,
|
||||
|
||||
@@ -181,6 +181,16 @@ const COMPACTION_CONTINUATION_RETRY_INSTRUCTION =
|
||||
"The previous attempt compacted the conversation context before producing a final user-visible answer. Continue from the compacted transcript and produce the final answer now. Do not restart from scratch, do not repeat completed work, and do not rerun tools unless the transcript clearly lacks required evidence.";
|
||||
type EmbeddedRunAttemptForRunner = Awaited<ReturnType<typeof runEmbeddedAttemptWithBackend>>;
|
||||
|
||||
function resolveHarnessContextConfigProvider(params: {
|
||||
provider: string;
|
||||
harnessId: string;
|
||||
}): string {
|
||||
if (params.harnessId === "codex" && params.provider.trim().toLowerCase() === "openai") {
|
||||
return "openai-codex";
|
||||
}
|
||||
return params.provider;
|
||||
}
|
||||
|
||||
function resolveEmbeddedRunLaneTimeoutMs(timeoutMs: number): number | undefined {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
||||
return undefined;
|
||||
@@ -530,6 +540,10 @@ export async function runEmbeddedPiAgent(
|
||||
const resolvedRuntimeModel = resolveEffectiveRuntimeModel({
|
||||
cfg: params.config,
|
||||
provider,
|
||||
contextConfigProvider: resolveHarnessContextConfigProvider({
|
||||
provider,
|
||||
harnessId: agentHarness.id,
|
||||
}),
|
||||
modelId,
|
||||
runtimeModel,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildBeforeModelResolveAttachments, resolveHookModelSelection } from "./setup.js";
|
||||
import type { ModelDefinitionConfig } from "../../../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import type { ProviderRuntimeModel } from "../../../plugins/provider-runtime-model.types.js";
|
||||
import {
|
||||
buildBeforeModelResolveAttachments,
|
||||
resolveEffectiveRuntimeModel,
|
||||
resolveHookModelSelection,
|
||||
} from "./setup.js";
|
||||
|
||||
const hookContext = {
|
||||
sessionId: "session-1",
|
||||
@@ -73,3 +80,90 @@ describe("resolveHookModelSelection", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function createRuntimeModel(): ProviderRuntimeModel {
|
||||
return {
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "gpt-5.5",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
api: "openai-responses",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
contextTokens: 272_000,
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
}
|
||||
|
||||
function createConfiguredModel(
|
||||
overrides: Partial<ModelDefinitionConfig> = {},
|
||||
): ModelDefinitionConfig {
|
||||
return {
|
||||
id: "gpt-5.5",
|
||||
name: "gpt-5.5",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
contextTokens: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveEffectiveRuntimeModel", () => {
|
||||
it("can read Codex OAuth context overrides for native Codex harness runs", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
models: [createConfiguredModel()],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const result = resolveEffectiveRuntimeModel({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
contextConfigProvider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
runtimeModel: createRuntimeModel(),
|
||||
});
|
||||
|
||||
expect(result.ctxInfo).toEqual({
|
||||
source: "modelsConfig",
|
||||
tokens: 1_000_000,
|
||||
});
|
||||
expect(result.effectiveModel.contextWindow).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("keeps the runtime model contextTokens when no alternate context provider is supplied", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
models: [createConfiguredModel()],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const result = resolveEffectiveRuntimeModel({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.5",
|
||||
runtimeModel: createRuntimeModel(),
|
||||
});
|
||||
|
||||
expect(result.ctxInfo).toEqual({
|
||||
source: "model",
|
||||
tokens: 272_000,
|
||||
});
|
||||
expect(result.effectiveModel.contextWindow).toBe(272_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,6 +117,7 @@ export function buildBeforeModelResolveAttachments(
|
||||
export function resolveEffectiveRuntimeModel(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
provider: string;
|
||||
contextConfigProvider?: string;
|
||||
modelId: string;
|
||||
runtimeModel: ProviderRuntimeModel;
|
||||
}): {
|
||||
@@ -125,7 +126,7 @@ export function resolveEffectiveRuntimeModel(params: {
|
||||
} {
|
||||
const ctxInfo = resolveContextWindowInfo({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
provider: params.contextConfigProvider ?? params.provider,
|
||||
modelId: params.modelId,
|
||||
modelContextTokens: readPiModelContextTokens(params.runtimeModel),
|
||||
modelContextWindow: params.runtimeModel.contextWindow,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
addSubagentRunForTests,
|
||||
resetSubagentRegistryForTests,
|
||||
} from "../../agents/subagent-registry.js";
|
||||
import type { ModelDefinitionConfig } from "../../config/types.models.js";
|
||||
import {
|
||||
completeTaskRunByRunId,
|
||||
createQueuedTaskRun,
|
||||
@@ -37,6 +38,16 @@ vi.mock("../../agents/harness/builtin-pi.js", () => ({
|
||||
}));
|
||||
|
||||
const baseCfg = baseCommandTestConfig;
|
||||
const codexStatusModel: ModelDefinitionConfig = {
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
contextTokens: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
|
||||
async function buildStatusReplyForTest(params: { sessionKey?: string; verbose?: boolean }) {
|
||||
const commandParams = buildCommandTestParams("/status", baseCfg);
|
||||
@@ -650,6 +661,52 @@ describe("buildStatusReply subagent summary", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses Codex OAuth context overrides for openai models running on the Codex harness", async () => {
|
||||
registerStatusCodexHarness();
|
||||
|
||||
const text = await buildStatusText({
|
||||
cfg: {
|
||||
...baseCfg,
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
models: [codexStatusModel],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
},
|
||||
},
|
||||
sessionEntry: {
|
||||
sessionId: "sess-status-codex-context",
|
||||
updatedAt: 0,
|
||||
totalTokens: 25_000,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
parentSessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
statusChannel: "mobilechat",
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
resolvedFastMode: false,
|
||||
resolvedVerboseLevel: "off",
|
||||
resolvedReasoningLevel: "off",
|
||||
resolveDefaultThinkingLevel: async () => undefined,
|
||||
isGroup: false,
|
||||
defaultGroupActivation: () => "mention",
|
||||
modelAuthOverride: "oauth",
|
||||
activeModelAuthOverride: "oauth",
|
||||
});
|
||||
|
||||
const normalized = normalizeTestText(text);
|
||||
expect(normalized).toContain("Model: openai/gpt-5.5");
|
||||
expect(normalized).toContain("Context: 25k/1.0m");
|
||||
});
|
||||
|
||||
it("uses workspace-scoped auth evidence in /status auth labels", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-status-auth-label-"));
|
||||
const workspaceDir = path.join(tempRoot, "workspace");
|
||||
|
||||
@@ -286,6 +286,7 @@ function resolveModelSelectionForCommand(params: {
|
||||
async function persistModelDirectiveForTest(params: {
|
||||
command: string;
|
||||
profiles?: Record<string, ApiKeyProfile>;
|
||||
cfg?: OpenClawConfig;
|
||||
aliasIndex?: ModelAliasIndex;
|
||||
allowedModelKeys: string[];
|
||||
sessionEntry?: SessionEntry;
|
||||
@@ -297,7 +298,7 @@ async function persistModelDirectiveForTest(params: {
|
||||
setAuthProfiles(params.profiles);
|
||||
}
|
||||
const directives = parseInlineDirectives(params.command);
|
||||
const cfg = baseConfig();
|
||||
const cfg = params.cfg ?? baseConfig();
|
||||
const sessionEntry = params.sessionEntry ?? createSessionEntry();
|
||||
const persisted = await persistInlineDirectives({
|
||||
directives,
|
||||
@@ -783,6 +784,39 @@ describe("/model chat UX", () => {
|
||||
expect(sessionEntry.agentRuntimeOverride).toBe("codex");
|
||||
});
|
||||
|
||||
it("uses Codex OAuth context config for persisted native Codex runtime directives", async () => {
|
||||
const { persisted } = await persistModelDirectiveForTest({
|
||||
command: "/model openai/gpt-5.5 --runtime codex hello",
|
||||
allowedModelKeys: ["openai/gpt-5.5"],
|
||||
cfg: {
|
||||
...baseConfig(),
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
contextTokens: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(persisted.provider).toBe("openai");
|
||||
expect(persisted.model).toBe("gpt-5.5");
|
||||
expect(persisted.contextTokens).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("clears runtime overrides when the model directive asks for default runtime", async () => {
|
||||
const { sessionEntry } = await persistModelDirectiveForTest({
|
||||
command: "/model openai/gpt-4o --runtime default hello",
|
||||
|
||||
@@ -3,8 +3,6 @@ import {
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveContextTokensForModel } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import type { ModelCatalogEntry } from "../../agents/model-catalog.js";
|
||||
import { listLegacyRuntimeModelProviderAliases } from "../../agents/model-runtime-aliases.js";
|
||||
import { normalizeProviderId, type ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
enqueueModeSwitchEvents,
|
||||
} from "./directive-handling.shared.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel } from "./directives.js";
|
||||
import { resolveContextTokens } from "./model-selection.js";
|
||||
|
||||
export type PersistedThinkingLevelRemap = {
|
||||
from: ThinkLevel;
|
||||
@@ -68,6 +67,29 @@ function resolveModelRuntimeOverride(params: {
|
||||
return { kind: "invalid", runtime: rawRuntime };
|
||||
}
|
||||
|
||||
function resolveContextConfigProviderForRuntime(params: {
|
||||
provider: string;
|
||||
runtimeId?: string;
|
||||
}): string {
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
const runtimeId = normalizeProviderId(params.runtimeId ?? "");
|
||||
if (provider === "openai" && runtimeId === "codex") {
|
||||
return "openai-codex";
|
||||
}
|
||||
return params.provider;
|
||||
}
|
||||
|
||||
function resolveDirectiveRuntimeId(params: {
|
||||
agentCfg: NonNullable<OpenClawConfig["agents"]>["defaults"] | undefined;
|
||||
sessionEntry?: SessionEntry;
|
||||
}): string | undefined {
|
||||
return (
|
||||
params.sessionEntry?.agentRuntimeOverride ??
|
||||
params.sessionEntry?.agentHarnessId ??
|
||||
params.agentCfg?.agentRuntime?.id
|
||||
);
|
||||
}
|
||||
|
||||
export async function persistInlineDirectives(params: {
|
||||
directives: InlineDirectives;
|
||||
effectiveModelDirective?: string;
|
||||
@@ -342,13 +364,14 @@ export async function persistInlineDirectives(params: {
|
||||
provider,
|
||||
model,
|
||||
thinkingRemap,
|
||||
contextTokens:
|
||||
resolveContextTokensForModel({
|
||||
cfg,
|
||||
contextTokens: resolveContextTokens({
|
||||
cfg,
|
||||
agentCfg,
|
||||
provider: resolveContextConfigProviderForRuntime({
|
||||
provider,
|
||||
model,
|
||||
contextTokensOverride: agentCfg?.contextTokens,
|
||||
allowAsyncLoad: false,
|
||||
}) ?? DEFAULT_CONTEXT_TOKENS,
|
||||
runtimeId: resolveDirectiveRuntimeId({ agentCfg, sessionEntry }),
|
||||
}),
|
||||
model,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,6 +221,32 @@ describe("resolveContextTokens", () => {
|
||||
|
||||
expect(result).toBe(1_000_000);
|
||||
});
|
||||
|
||||
it("treats agent contextTokens as a cap, not an expansion beyond the model window", () => {
|
||||
MODEL_CONTEXT_TOKEN_CACHE.set("openai/gpt-5.5", 272_000);
|
||||
|
||||
const result = resolveContextTokens({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentCfg: { contextTokens: 1_000_000 },
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
});
|
||||
|
||||
expect(result).toBe(272_000);
|
||||
});
|
||||
|
||||
it("allows agent contextTokens to lower a larger model window", () => {
|
||||
MODEL_CONTEXT_TOKEN_CACHE.set("qwen/qwen3.6-plus", 1_000_000);
|
||||
|
||||
const result = resolveContextTokens({
|
||||
cfg: {} as OpenClawConfig,
|
||||
agentCfg: { contextTokens: 180_000 },
|
||||
provider: "qwen",
|
||||
model: "qwen3.6-plus",
|
||||
});
|
||||
|
||||
expect(result).toBe(180_000);
|
||||
});
|
||||
});
|
||||
|
||||
const makeEntry = (overrides: Partial<SessionEntry> = {}): SessionEntry => ({
|
||||
|
||||
@@ -328,14 +328,22 @@ export function resolveContextTokens(params: {
|
||||
provider: string;
|
||||
model: string;
|
||||
}): number {
|
||||
return (
|
||||
params.agentCfg?.contextTokens ??
|
||||
resolveContextTokensForModel({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
allowAsyncLoad: false,
|
||||
}) ??
|
||||
DEFAULT_CONTEXT_TOKENS
|
||||
);
|
||||
const modelContextTokens = resolveContextTokensForModel({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
allowAsyncLoad: false,
|
||||
});
|
||||
const agentContextTokens =
|
||||
typeof params.agentCfg?.contextTokens === "number" && params.agentCfg.contextTokens > 0
|
||||
? Math.floor(params.agentCfg.contextTokens)
|
||||
: undefined;
|
||||
|
||||
if (agentContextTokens !== undefined) {
|
||||
return modelContextTokens !== undefined
|
||||
? Math.min(agentContextTokens, modelContextTokens)
|
||||
: agentContextTokens;
|
||||
}
|
||||
|
||||
return modelContextTokens ?? DEFAULT_CONTEXT_TOKENS;
|
||||
}
|
||||
|
||||
@@ -1967,6 +1967,54 @@ describe("buildStatusMessage", () => {
|
||||
expect(normalized).not.toContain("Context: 25k/200k");
|
||||
});
|
||||
|
||||
it("does not let agent contextTokens inflate status above the model window", () => {
|
||||
MODEL_CONTEXT_TOKEN_CACHE.set("openai/gpt-5.5", 272_000);
|
||||
|
||||
const text = buildStatusMessage({
|
||||
agent: {
|
||||
model: "openai/gpt-5.5",
|
||||
contextTokens: 1_000_000,
|
||||
},
|
||||
sessionEntry: {
|
||||
sessionId: "sess-openai-codex-cap-context",
|
||||
updatedAt: 0,
|
||||
totalTokens: 25_000,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
modelAuth: "oauth",
|
||||
});
|
||||
|
||||
const normalized = normalizeTestText(text);
|
||||
expect(normalized).toContain("Context: 25k/272k");
|
||||
expect(normalized).not.toContain("Context: 25k/1.0m");
|
||||
});
|
||||
|
||||
it("uses runtime context tokens to cap status when the sync cache is cold", () => {
|
||||
const text = buildStatusMessage({
|
||||
agent: {
|
||||
model: "openai/gpt-5.5",
|
||||
contextTokens: 1_000_000,
|
||||
},
|
||||
explicitConfiguredContextTokens: 1_000_000,
|
||||
runtimeContextTokens: 272_000,
|
||||
sessionEntry: {
|
||||
sessionId: "sess-openai-codex-runtime-cap-context",
|
||||
updatedAt: 0,
|
||||
totalTokens: 25_000,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
modelAuth: "oauth",
|
||||
});
|
||||
|
||||
const normalized = normalizeTestText(text);
|
||||
expect(normalized).toContain("Context: 25k/272k");
|
||||
expect(normalized).not.toContain("Context: 25k/1.0m");
|
||||
});
|
||||
|
||||
it("does not synthesize a 32k fallback window when the active runtime model is unknown", () => {
|
||||
const text = buildStatusMessage({
|
||||
config: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import type WebSocket from "ws";
|
||||
import { resetConfigRuntimeState } from "../config/config.js";
|
||||
import type { GuardedFetchOptions } from "../infra/net/fetch-guard.js";
|
||||
import type { GatewayCronState } from "./server-cron.js";
|
||||
import {
|
||||
connectOk,
|
||||
cronIsolatedRun,
|
||||
@@ -156,9 +157,7 @@ async function setupCronTestRun(params: {
|
||||
return { prevSkipCron, dir };
|
||||
}
|
||||
|
||||
type DirectCronState = {
|
||||
cron: { start: () => Promise<void>; stop: () => void };
|
||||
storePath: string;
|
||||
type DirectCronState = GatewayCronState & {
|
||||
getRuntimeConfig: () => import("../config/types.openclaw.js").OpenClawConfig;
|
||||
};
|
||||
|
||||
@@ -191,12 +190,13 @@ function createCronEventCollector() {
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}> = [];
|
||||
const flush = (payload: Record<string, unknown>) => {
|
||||
for (const waiter of [...waiters]) {
|
||||
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
||||
const waiter = waiters[index];
|
||||
if (!waiter.check(payload)) {
|
||||
continue;
|
||||
}
|
||||
clearTimeout(waiter.timer);
|
||||
waiters.splice(waiters.indexOf(waiter), 1);
|
||||
waiters.splice(index, 1);
|
||||
waiter.resolve(payload);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,7 +153,8 @@ async function mapWithConcurrency<T, U>(
|
||||
concurrency: number,
|
||||
fn: (item: T) => Promise<U>,
|
||||
): Promise<U[]> {
|
||||
const results = new Array<U>(items.length);
|
||||
const results: U[] = [];
|
||||
results.length = items.length;
|
||||
let nextIndex = 0;
|
||||
const workerCount = Math.min(concurrency, items.length);
|
||||
await Promise.all(
|
||||
@@ -161,7 +162,7 @@ async function mapWithConcurrency<T, U>(
|
||||
while (nextIndex < items.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
results[index] = await fn(items[index]!);
|
||||
results[index] = await fn(items[index]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -651,12 +651,17 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
model: selectedModel,
|
||||
allowAsyncLoad: false,
|
||||
});
|
||||
const activeContextTokens = resolveContextTokensForModel({
|
||||
cfg: contextConfig,
|
||||
...(contextLookupProvider ? { provider: contextLookupProvider } : {}),
|
||||
model: contextLookupModel,
|
||||
allowAsyncLoad: false,
|
||||
});
|
||||
const explicitRuntimeContextTokens =
|
||||
typeof args.runtimeContextTokens === "number" && args.runtimeContextTokens > 0
|
||||
? args.runtimeContextTokens
|
||||
: undefined;
|
||||
const activeContextTokens =
|
||||
resolveContextTokensForModel({
|
||||
cfg: contextConfig,
|
||||
...(contextLookupProvider ? { provider: contextLookupProvider } : {}),
|
||||
model: contextLookupModel,
|
||||
allowAsyncLoad: false,
|
||||
}) ?? explicitRuntimeContextTokens;
|
||||
const channelModelNote = resolveChannelModelNote({
|
||||
config: args.config,
|
||||
entry,
|
||||
@@ -672,10 +677,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
typeof args.agent?.contextTokens === "number" && args.agent.contextTokens > 0
|
||||
? args.agent.contextTokens
|
||||
: undefined;
|
||||
const explicitRuntimeContextTokens =
|
||||
typeof args.runtimeContextTokens === "number" && args.runtimeContextTokens > 0
|
||||
? args.runtimeContextTokens
|
||||
: undefined;
|
||||
const explicitConfiguredContextTokens =
|
||||
typeof args.explicitConfiguredContextTokens === "number" &&
|
||||
args.explicitConfiguredContextTokens > 0
|
||||
@@ -687,14 +688,18 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
? Math.min(explicitConfiguredContextTokens, activeContextTokens)
|
||||
: explicitConfiguredContextTokens
|
||||
: undefined;
|
||||
const cappedAgentContextTokens =
|
||||
typeof agentContextTokens === "number"
|
||||
? typeof activeContextTokens === "number"
|
||||
? Math.min(agentContextTokens, activeContextTokens)
|
||||
: agentContextTokens
|
||||
: undefined;
|
||||
const channelOverrideContextTokens = channelModelNote
|
||||
? (explicitRuntimeContextTokens ??
|
||||
cappedConfiguredContextTokens ??
|
||||
(typeof activeContextTokens === "number"
|
||||
? typeof agentContextTokens === "number"
|
||||
? Math.min(agentContextTokens, activeContextTokens)
|
||||
: activeContextTokens
|
||||
: agentContextTokens))
|
||||
? (cappedAgentContextTokens ?? activeContextTokens)
|
||||
: cappedAgentContextTokens))
|
||||
: undefined;
|
||||
// When a fallback model is active, the selected-model context limit that
|
||||
// callers keep on the agent config is often stale. Prefer an explicit runtime
|
||||
@@ -743,7 +748,11 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
...(contextLookupProvider ? { provider: contextLookupProvider } : {}),
|
||||
model: contextLookupModel,
|
||||
contextTokensOverride:
|
||||
channelOverrideContextTokens ?? persistedContextTokens ?? agentContextTokens,
|
||||
channelOverrideContextTokens ??
|
||||
persistedContextTokens ??
|
||||
cappedConfiguredContextTokens ??
|
||||
cappedAgentContextTokens ??
|
||||
explicitRuntimeContextTokens,
|
||||
fallbackContextTokens: DEFAULT_CONTEXT_TOKENS,
|
||||
allowAsyncLoad: false,
|
||||
}) ?? DEFAULT_CONTEXT_TOKENS);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveSessionAgentId,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { resolveFastModeState } from "../agents/fast-mode.js";
|
||||
import { resolveModelAuthLabel } from "../agents/model-auth-label.js";
|
||||
import {
|
||||
@@ -81,6 +82,19 @@ function loadStatusQueueRuntime(): Promise<typeof import("./status-queue.runtime
|
||||
return runtimePromise;
|
||||
}
|
||||
|
||||
function resolveStatusRuntimeContextTokens(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
model: string;
|
||||
}): number | undefined {
|
||||
return resolveContextTokensForModel({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
allowAsyncLoad: false,
|
||||
});
|
||||
}
|
||||
|
||||
function shouldLoadUsageSummary(params: {
|
||||
provider?: string;
|
||||
selectedModelAuth?: string;
|
||||
@@ -342,6 +356,15 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
const explicitThinkingDefault =
|
||||
(agentConfig?.thinkingDefault as ThinkLevel | undefined) ??
|
||||
(agentDefaults.thinkingDefault as ThinkLevel | undefined);
|
||||
const runtimeContextProvider = resolveStatusAuthProvider({
|
||||
provider: modelRefs.active.provider || provider,
|
||||
effectiveHarness,
|
||||
});
|
||||
const runtimeContextTokens = resolveStatusRuntimeContextTokens({
|
||||
cfg,
|
||||
provider: runtimeContextProvider,
|
||||
model: modelRefs.active.model || model,
|
||||
});
|
||||
return buildStatusMessage({
|
||||
config: cfg,
|
||||
agent: {
|
||||
@@ -362,6 +385,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
|
||||
typeof agentDefaults.contextTokens === "number" && agentDefaults.contextTokens > 0
|
||||
? agentDefaults.contextTokens
|
||||
: undefined,
|
||||
runtimeContextTokens,
|
||||
sessionEntry,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
|
||||
Reference in New Issue
Block a user