From dd43caa27aa1a805af6a4f1bdaa9a7122d609100 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 2 May 2026 16:10:13 -0700 Subject: [PATCH] Fix Trinity main-session compatibility mismatch (#73388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 4c669d66cb15d6fd0804900d337cbf373aeee9f6. - Required merge gates passed before the squash merge. Prepared head SHA: 4c669d66cb15d6fd0804900d337cbf373aeee9f6 Review: https://github.com/openclaw/openclaw/pull/73388#issuecomment-4338585215 Co-authored-by: Vincent Koc Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/providers/arcee.md | 24 +-- extensions/arcee/index.test.ts | 176 +++++++++++++++++++ extensions/arcee/index.ts | 19 +- extensions/arcee/models.ts | 9 +- extensions/arcee/provider-catalog.ts | 36 +--- extensions/arcee/provider-policy-api.test.ts | 73 ++++++++ extensions/arcee/provider-policy-api.ts | 11 ++ extensions/arcee/provider-policy.ts | 132 ++++++++++++++ 9 files changed, 430 insertions(+), 51 deletions(-) create mode 100644 extensions/arcee/provider-policy-api.test.ts create mode 100644 extensions/arcee/provider-policy-api.ts create mode 100644 extensions/arcee/provider-policy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5829546cc63..7a769cd7e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/providers/arcee.md b/docs/providers/arcee.md index 90e0523cca2..b08f7a86c22 100644 --- a/docs/providers/arcee.md +++ b/docs/providers/arcee.md @@ -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 | -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. ## 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) | diff --git a/extensions/arcee/index.test.ts b/extensions/arcee/index.test.ts index 8379057b443..a8ed79207f0 100644 --- a/extensions/arcee/index.test.ts +++ b/extensions/arcee/index.test.ts @@ -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(); + }); }); diff --git a/extensions/arcee/index.ts b/extensions/arcee/index.ts index e7fd0126950..edd4fda7aa3 100644 --- a/extensions/arcee/index.ts +++ b/extensions/arcee/index.ts @@ -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 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 diff --git a/extensions/arcee/models.ts b/extensions/arcee/models.ts index 399faed49a8..cb8873301f2 100644 --- a/extensions/arcee/models.ts +++ b/extensions/arcee/models.ts @@ -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, }, ]; diff --git a/extensions/arcee/provider-catalog.ts b/extensions/arcee/provider-catalog.ts index 5631ad2998d..844d9a68dd6 100644 --- a/extensions/arcee/provider-catalog.ts +++ b/extensions/arcee/provider-catalog.ts @@ -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 { return ARCEE_MODEL_CATALOG.map(buildArceeModelDefinition); diff --git a/extensions/arcee/provider-policy-api.test.ts b/extensions/arcee/provider-policy-api.test.ts new file mode 100644 index 00000000000..0d4afe464d2 --- /dev/null +++ b/extensions/arcee/provider-policy-api.test.ts @@ -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[0]["providerConfig"]; + + expect(normalizeConfig({ provider: "arcee", providerConfig })).toBe(providerConfig); + }); +}); diff --git a/extensions/arcee/provider-policy-api.ts b/extensions/arcee/provider-policy-api.ts new file mode 100644 index 00000000000..61fffb9057f --- /dev/null +++ b/extensions/arcee/provider-policy-api.ts @@ -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); +} diff --git a/extensions/arcee/provider-policy.ts b/extensions/arcee/provider-policy.ts new file mode 100644 index 00000000000..a02cf9336cd --- /dev/null +++ b/extensions/arcee/provider-policy.ts @@ -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( + model: T, +): T { + if (!isArceeTrinityLargeThinkingModelId(model.id)) { + return model; + } + const compat = + model.compat && typeof model.compat === "object" + ? (model.compat as Record) + : 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; +}