Fix Trinity main-session compatibility mismatch (#73388)

Summary:
- The PR marks Arcee Trinity Large Thinking tool-incompatible in catalog/config/runtime paths, updates Arcee docs and changelog, and adds provider regression tests.
- Reproducibility: yes. The linked reports provide concrete main-session failure logs, and current main still exposes Trinity without `compat.supportsTools:false` while the runtime sends tools unless that flag is false.

ClawSweeper fixups:
- Included follow-up commit: fix(arcee): disable Trinity tools in main sessions
- Included follow-up commit: fix(clawsweeper): address review for automerge-openclaw-openclaw-7338…
- Included follow-up commit: fix(arcee): repair Trinity main-session compatibility

Validation:
- ClawSweeper review passed for head 4c669d66cb.
- Required merge gates passed before the squash merge.

Prepared head SHA: 4c669d66cb
Review: https://github.com/openclaw/openclaw/pull/73388#issuecomment-4338585215

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
Vincent Koc
2026-05-02 16:10:13 -07:00
committed by GitHub
parent 68359cacbf
commit dd43caa27a
9 changed files with 430 additions and 51 deletions

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing `main` sessions from staying stuck as running after completed or timed-out turns.
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
- Heartbeat/scheduler: make heartbeat phase scheduling active-hours-aware so the scheduler seeks forward to the first in-window phase slot instead of arming timers for quiet-hours slots and relying solely on the runtime guard. Non-UTC `activeHours.timezone` values (e.g. `Asia/Shanghai`) now correctly influence when the next heartbeat timer fires, avoiding wasted quiet-hours ticks and long dormant gaps after gateway restarts. Fixes #75487. Thanks @amknight.
- Providers/Arcee AI: mark Trinity Large Thinking as tool-incompatible so main-session runs use the same text-only request shape that made subagent runs recover, avoiding the remaining main-session response-shape mismatch after the #62848 transport failover fix. Fixes #62851 and #62847; carries forward #62848. Thanks @Adam-Researchh.
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.

View File

@@ -98,24 +98,24 @@ Arcee AI models can be accessed directly via the Arcee platform or through [Open
OpenClaw currently ships this bundled Arcee catalog:
| Model ref | Name | Input | Context | Cost (in/out per 1M) | Notes |
| ------------------------------ | ---------------------- | ----- | ------- | -------------------- | ----------------------------------------- |
| `arcee/trinity-large-thinking` | Trinity Large Thinking | text | 256K | $0.25 / $0.90 | Default model; reasoning enabled |
| `arcee/trinity-large-preview` | Trinity Large Preview | text | 128K | $0.25 / $1.00 | General-purpose; 400B params, 13B active |
| `arcee/trinity-mini` | Trinity Mini 26B | text | 128K | $0.045 / $0.15 | Fast and cost-efficient; function calling |
| Model ref | Name | Input | Context | Cost (in/out per 1M) | Notes |
| ------------------------------ | ---------------------- | ----- | ------- | -------------------- | ------------------------------------------ |
| `arcee/trinity-large-thinking` | Trinity Large Thinking | text | 256K | $0.25 / $0.90 | Default model; reasoning enabled; no tools |
| `arcee/trinity-large-preview` | Trinity Large Preview | text | 128K | $0.25 / $1.00 | General-purpose; 400B params, 13B active |
| `arcee/trinity-mini` | Trinity Mini 26B | text | 128K | $0.045 / $0.15 | Fast and cost-efficient; function calling |
<Tip>
The onboarding preset sets `arcee/trinity-large-thinking` as the default model.
The onboarding preset sets `arcee/trinity-large-thinking` as the default model. It is reasoning/text-only and does not support tool use or function calling.
</Tip>
## Supported features
| Feature | Supported |
| --------------------------------------------- | ---------------------------- |
| Streaming | Yes |
| Tool use / function calling | Yes |
| Structured output (JSON mode and JSON schema) | Yes |
| Extended thinking | Yes (Trinity Large Thinking) |
| Feature | Supported |
| --------------------------------------------- | ------------------------------------------- |
| Streaming | Yes |
| Tool use / function calling | Model-dependent; not Trinity Large Thinking |
| Structured output (JSON mode and JSON schema) | Yes |
| Extended thinking | Yes (Trinity Large Thinking) |
<AccordionGroup>
<Accordion title="Environment note">

View File

@@ -69,6 +69,14 @@ describe("arcee provider plugin", () => {
"arcee/trinity-large-preview",
"arcee/trinity-large-thinking",
]);
expect(
config?.models?.providers?.arcee?.models?.find(
(model) => model.id === "arcee/trinity-large-thinking",
)?.compat,
).toMatchObject({
supportsReasoningEffort: false,
supportsTools: false,
});
});
it("keeps direct Arcee auth env candidates separate from OpenRouter", () => {
@@ -92,6 +100,12 @@ describe("arcee provider plugin", () => {
"trinity-large-preview",
"trinity-large-thinking",
]);
expect(
catalogProvider.models?.find((model) => model.id === "trinity-large-thinking")?.compat,
).toMatchObject({
supportsReasoningEffort: false,
supportsTools: false,
});
});
it("builds the OpenRouter-backed Arcee AI model catalog", async () => {
@@ -112,6 +126,12 @@ describe("arcee provider plugin", () => {
"arcee/trinity-large-preview",
"arcee/trinity-large-thinking",
]);
expect(
catalogProvider.models?.find((model) => model.id === "arcee/trinity-large-thinking")?.compat,
).toMatchObject({
supportsReasoningEffort: false,
supportsTools: false,
});
});
it("normalizes Arcee OpenRouter models to vendor-prefixed runtime ids", async () => {
@@ -130,6 +150,10 @@ describe("arcee provider plugin", () => {
} as never),
).toMatchObject({
id: "arcee/trinity-large-thinking",
compat: {
supportsReasoningEffort: false,
supportsTools: false,
},
});
expect(
@@ -176,6 +200,10 @@ describe("arcee provider plugin", () => {
).toMatchObject({
id: "arcee/trinity-large-thinking",
baseUrl: "https://openrouter.ai/api/v1",
compat: {
supportsReasoningEffort: false,
supportsTools: false,
},
});
expect(
@@ -189,4 +217,152 @@ describe("arcee provider plugin", () => {
baseUrl: "https://openrouter.ai/api/v1",
});
});
it("repairs stale Trinity tool compat on existing Arcee configs and runtime models", async () => {
const provider = await registerSingleProviderPlugin(arceePlugin);
expect(
provider.normalizeConfig?.({
provider: "arcee",
providerConfig: {
api: "openai-completions",
baseUrl: "https://openrouter.ai/v1/",
models: [
{
id: "arcee/trinity-large-thinking",
name: "Trinity Large Thinking",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 80000,
cost: {
input: 0.25,
output: 0.9,
cacheRead: 0.25,
cacheWrite: 0.25,
},
compat: {
supportsReasoningEffort: false,
supportsStrictMode: true,
},
},
],
},
} as never),
).toMatchObject({
baseUrl: "https://openrouter.ai/api/v1",
models: [
{
id: "arcee/trinity-large-thinking",
compat: {
supportsReasoningEffort: false,
supportsStrictMode: true,
supportsTools: false,
},
},
],
});
expect(
provider.normalizeConfig?.({
provider: "arcee",
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.arcee.ai/api/v1",
models: [
{
id: "trinity-large-thinking",
name: "Trinity Large Thinking",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 80000,
cost: {
input: 0.25,
output: 0.9,
cacheRead: 0.25,
cacheWrite: 0.25,
},
compat: {
supportsReasoningEffort: false,
},
},
],
},
} as never),
).toMatchObject({
baseUrl: "https://api.arcee.ai/api/v1",
models: [
{
id: "trinity-large-thinking",
compat: {
supportsReasoningEffort: false,
supportsTools: false,
},
},
],
});
const trinityRuntimeModel = {
name: "Trinity Large Thinking",
api: "openai-completions",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 80000,
cost: {
input: 0.25,
output: 0.9,
cacheRead: 0.25,
cacheWrite: 0.25,
},
compat: {
supportsReasoningEffort: false,
},
};
const trinityCompat = {
supportsReasoningEffort: false,
supportsTools: false,
};
expect(
provider.contributeResolvedModelCompat?.({
provider: "arcee",
modelId: "arcee/trinity-large-thinking",
model: {
...trinityRuntimeModel,
provider: "arcee",
id: "arcee/trinity-large-thinking",
baseUrl: "https://openrouter.ai/api/v1",
},
} as never),
).toEqual(trinityCompat);
expect(
provider.contributeResolvedModelCompat?.({
provider: "arcee",
modelId: "trinity-large-thinking",
model: {
...trinityRuntimeModel,
provider: "arcee",
id: "trinity-large-thinking",
baseUrl: "https://api.arcee.ai/api/v1",
},
} as never),
).toEqual(trinityCompat);
expect(
provider.contributeResolvedModelCompat?.({
provider: "openrouter",
modelId: "trinity-large-thinking",
model: {
...trinityRuntimeModel,
provider: "openrouter",
id: "trinity-large-thinking",
baseUrl: "https://openrouter.ai/api/v1",
},
} as never),
).toBeUndefined();
});
});

View File

@@ -17,6 +17,12 @@ import {
normalizeArceeOpenRouterBaseUrl,
toArceeOpenRouterModelId,
} from "./provider-catalog.js";
import {
ARCEE_TRINITY_LARGE_THINKING_COMPAT,
applyArceeTrinityLargeThinkingCompat,
normalizeArceeProviderConfig,
shouldContributeArceeTrinityLargeThinkingCompat,
} from "./provider-policy.js";
const PROVIDER_ID = "arcee";
const ARCEE_WIZARD_GROUP = {
@@ -95,7 +101,7 @@ function normalizeArceeResolvedModel<T extends { baseUrl?: string; id: string }>
return undefined;
}
return {
...model,
...applyArceeTrinityLargeThinkingCompat(model),
id: normalizedId,
baseUrl: normalizedBaseUrl,
};
@@ -120,13 +126,12 @@ export default definePluginEntry({
config,
providerId: PROVIDER_ID,
}),
normalizeConfig: ({ providerConfig }) => {
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(providerConfig.baseUrl);
return normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl
? { ...providerConfig, baseUrl: normalizedBaseUrl }
: undefined;
},
normalizeConfig: ({ providerConfig }) => normalizeArceeProviderConfig(providerConfig),
normalizeResolvedModel: ({ model }) => normalizeArceeResolvedModel(model),
contributeResolvedModelCompat: (ctx) =>
shouldContributeArceeTrinityLargeThinkingCompat(ctx)
? ARCEE_TRINITY_LARGE_THINKING_COMPAT
: undefined,
normalizeTransport: ({ api, baseUrl }) => {
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(baseUrl);
return normalizedBaseUrl && normalizedBaseUrl !== baseUrl

View File

@@ -1,6 +1,7 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-types";
import { ARCEE_BASE_URL, ARCEE_TRINITY_LARGE_THINKING_COMPAT } from "./provider-policy.js";
export const ARCEE_BASE_URL = "https://api.arcee.ai/api/v1";
export { ARCEE_BASE_URL, ARCEE_TRINITY_LARGE_THINKING_COMPAT };
export const ARCEE_MODEL_CATALOG: ModelDefinitionConfig[] = [
{
@@ -44,9 +45,7 @@ export const ARCEE_MODEL_CATALOG: ModelDefinitionConfig[] = [
cacheRead: 0.25,
cacheWrite: 0.25,
},
compat: {
supportsReasoningEffort: false,
},
compat: ARCEE_TRINITY_LARGE_THINKING_COMPAT,
},
];

View File

@@ -1,31 +1,13 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./models.js";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { buildArceeModelDefinition, ARCEE_MODEL_CATALOG } from "./models.js";
import {
ARCEE_BASE_URL,
normalizeArceeOpenRouterBaseUrl,
OPENROUTER_BASE_URL,
toArceeOpenRouterModelId,
} from "./provider-policy.js";
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
const OPENROUTER_LEGACY_BASE_URL = "https://openrouter.ai/v1";
function normalizeBaseUrl(baseUrl: string | undefined): string {
return (baseUrl ?? "").trim().replace(/\/+$/, "");
}
export function normalizeArceeOpenRouterBaseUrl(baseUrl: string | undefined): string | undefined {
const normalized = normalizeBaseUrl(baseUrl);
if (!normalized) {
return undefined;
}
if (normalized === OPENROUTER_BASE_URL || normalized === OPENROUTER_LEGACY_BASE_URL) {
return OPENROUTER_BASE_URL;
}
return undefined;
}
export function toArceeOpenRouterModelId(modelId: string): string {
const normalized = modelId.trim();
if (!normalized || normalized.startsWith("arcee/")) {
return normalized;
}
return `arcee/${normalized}`;
}
export { normalizeArceeOpenRouterBaseUrl, OPENROUTER_BASE_URL, toArceeOpenRouterModelId };
export function buildArceeCatalogModels(): NonNullable<ModelProviderConfig["models"]> {
return ARCEE_MODEL_CATALOG.map(buildArceeModelDefinition);

View File

@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { normalizeConfig } from "./provider-policy-api.js";
describe("arcee provider policy public artifact", () => {
it("normalizes stale OpenRouter base URLs and Trinity compat without loading the full plugin", () => {
expect(
normalizeConfig({
provider: "arcee",
providerConfig: {
api: "openai-completions",
baseUrl: "https://openrouter.ai/v1/",
models: [
{
id: "arcee/trinity-large-thinking",
name: "Trinity Large Thinking",
reasoning: true,
input: ["text"],
contextWindow: 262144,
maxTokens: 80000,
cost: {
input: 0.25,
output: 0.9,
cacheRead: 0.25,
cacheWrite: 0.25,
},
compat: {
supportsReasoningEffort: false,
supportsStrictMode: true,
},
},
],
},
}),
).toMatchObject({
baseUrl: "https://openrouter.ai/api/v1",
models: [
{
id: "arcee/trinity-large-thinking",
compat: {
supportsReasoningEffort: false,
supportsStrictMode: true,
supportsTools: false,
},
},
],
});
});
it("returns unchanged non-Trinity configs by identity", () => {
const providerConfig = {
api: "openai-completions",
baseUrl: "https://api.arcee.ai/api/v1",
models: [
{
id: "trinity-mini",
name: "Trinity Mini 26B",
reasoning: false,
input: ["text"],
contextWindow: 131072,
maxTokens: 80000,
cost: {
input: 0.045,
output: 0.15,
cacheRead: 0.045,
cacheWrite: 0.045,
},
},
],
} satisfies Parameters<typeof normalizeConfig>[0]["providerConfig"];
expect(normalizeConfig({ provider: "arcee", providerConfig })).toBe(providerConfig);
});
});

View File

@@ -0,0 +1,11 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { normalizeArceeProviderConfig } from "./provider-policy.js";
export { normalizeArceeProviderConfig };
export function normalizeConfig(params: {
provider?: string;
providerConfig: ModelProviderConfig;
}): ModelProviderConfig {
return normalizeArceeProviderConfig(params.providerConfig);
}

View File

@@ -0,0 +1,132 @@
import type {
ModelCompatConfig,
ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-types";
export const ARCEE_BASE_URL = "https://api.arcee.ai/api/v1";
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
export const ARCEE_TRINITY_LARGE_THINKING_COMPAT = {
supportsReasoningEffort: false,
supportsTools: false,
} as const satisfies ModelCompatConfig;
const ARCEE_PROVIDER_ID = "arcee";
const OPENROUTER_LEGACY_BASE_URL = "https://openrouter.ai/v1";
const ARCEE_TRINITY_LARGE_THINKING_ID = "trinity-large-thinking";
const ARCEE_TRINITY_LARGE_THINKING_REF = `${ARCEE_PROVIDER_ID}/${ARCEE_TRINITY_LARGE_THINKING_ID}`;
function normalizeModelId(modelId: string): string {
return modelId.trim().toLowerCase();
}
function normalizeBaseUrl(baseUrl: unknown): string {
return typeof baseUrl === "string" ? baseUrl.trim().replace(/\/+$/, "") : "";
}
export function normalizeArceeOpenRouterBaseUrl(baseUrl: string | undefined): string | undefined {
const normalized = normalizeBaseUrl(baseUrl);
if (!normalized) {
return undefined;
}
if (normalized === OPENROUTER_BASE_URL || normalized === OPENROUTER_LEGACY_BASE_URL) {
return OPENROUTER_BASE_URL;
}
return undefined;
}
export function toArceeOpenRouterModelId(modelId: string): string {
const normalized = modelId.trim();
if (!normalized || normalized.startsWith("arcee/")) {
return normalized;
}
return `arcee/${normalized}`;
}
export function isArceeTrinityLargeThinkingModelId(modelId: string): boolean {
const normalized = normalizeModelId(modelId);
return (
normalized === ARCEE_TRINITY_LARGE_THINKING_ID ||
normalized === ARCEE_TRINITY_LARGE_THINKING_REF
);
}
export function shouldContributeArceeTrinityLargeThinkingCompat(params: {
provider?: unknown;
modelId: string;
model: { id: string; provider?: unknown; baseUrl?: unknown };
}): boolean {
const modelId = normalizeModelId(params.modelId);
const resolvedId = normalizeModelId(params.model.id);
if (
modelId === ARCEE_TRINITY_LARGE_THINKING_REF ||
resolvedId === ARCEE_TRINITY_LARGE_THINKING_REF
) {
return true;
}
if (
modelId !== ARCEE_TRINITY_LARGE_THINKING_ID &&
resolvedId !== ARCEE_TRINITY_LARGE_THINKING_ID
) {
return false;
}
if (params.provider === ARCEE_PROVIDER_ID || params.model.provider === ARCEE_PROVIDER_ID) {
return true;
}
return normalizeBaseUrl(params.model.baseUrl) === normalizeBaseUrl(ARCEE_BASE_URL);
}
export function applyArceeTrinityLargeThinkingCompat<T extends { id: string; compat?: unknown }>(
model: T,
): T {
if (!isArceeTrinityLargeThinkingModelId(model.id)) {
return model;
}
const compat =
model.compat && typeof model.compat === "object"
? (model.compat as Record<string, unknown>)
: undefined;
if (
compat?.supportsReasoningEffort ===
ARCEE_TRINITY_LARGE_THINKING_COMPAT.supportsReasoningEffort &&
compat?.supportsTools === ARCEE_TRINITY_LARGE_THINKING_COMPAT.supportsTools
) {
return model;
}
return {
...model,
compat: {
...compat,
...ARCEE_TRINITY_LARGE_THINKING_COMPAT,
} as T extends { compat?: infer TCompat } ? TCompat : never,
} as T;
}
export function normalizeArceeProviderConfig(
providerConfig: ModelProviderConfig,
): ModelProviderConfig {
let changed = false;
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(providerConfig.baseUrl);
const baseUrl =
normalizedBaseUrl && normalizedBaseUrl !== providerConfig.baseUrl
? normalizedBaseUrl
: providerConfig.baseUrl;
if (baseUrl !== providerConfig.baseUrl) {
changed = true;
}
const hasModels = Array.isArray(providerConfig.models);
const models = hasModels
? providerConfig.models.map((model) => {
const normalizedModel = applyArceeTrinityLargeThinkingCompat(model);
if (normalizedModel === model) {
return model;
}
changed = true;
return normalizedModel;
})
: providerConfig.models;
return changed
? { ...providerConfig, baseUrl, ...(hasModels ? { models } : {}) }
: providerConfig;
}