fix: add vercel ai gateway thinking profile

Adds a Vercel AI Gateway provider thinking-profile resolver for trusted OpenAI and Anthropic upstream refs, preserving catalog compat fallback for unsupported/base-only refs.

Includes provider tests, docs, and changelog coverage. Supersedes #41561.

Co-authored-by: Zcg2021 <80769518+Zcg2021@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-29 12:13:20 +01:00
committed by GitHub
parent 6d7a77dcf9
commit 9d1c5a77c2
5 changed files with 161 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- Cron/Gateway: defer missed isolated agent-turn catch-up out of the channel startup window, so overdue cron work cannot starve Discord or Telegram while providers connect after a restart. Thanks @vincentkoc.
- Heartbeat/cron: defer heartbeat turns while cron work is active or queued, add opt-in `heartbeat.skipWhenBusy` for subagent/nested lane pressure, and retry busy skips without advancing the schedule so local Ollama hosts do not run heartbeat and cron prompts concurrently. Fixes #50773. Thanks @scottgl9.
- Agents/thinking: honor configured model `compat.supportedReasoningEfforts` entries that include `xhigh`, so custom OpenAI-compatible provider refs expose and validate `/think xhigh` consistently across command menus, Gateway sessions, agent CLI, and `llm-task`. Carries forward #48904. Thanks @Milchstrassse and @wufunc.
- Vercel AI Gateway: expose provider-owned `/think xhigh` for trusted OpenAI/Codex upstream refs and Claude adaptive thinking for Anthropic upstream refs, while leaving untrusted namespaced refs on base levels. Carries forward #41561. Thanks @Zcg2021.
- Plugins/runtime-deps: prune stale `openclaw-unknown-*` bundled runtime dependency roots during Gateway startup while keeping recent or locked roots, so old staging debris cannot keep growing across restarts. Thanks @vincentkoc.
- Ollama: compose caller abort signals with guarded-fetch timeouts for native `/api/chat` streams, so `/stop` and early cancellation still interrupt local Ollama requests that also carry provider timeout budgets. Refs #74133. Thanks @obviyus.
- Doctor/TTS: migrate legacy `messages.tts.enabled`, agent TTS, channel TTS, and voice-call plugin TTS toggles to `auto` mode during `openclaw doctor --fix`, matching the documented TTS config contract. Thanks @vincentkoc.

View File

@@ -105,6 +105,15 @@ configuration. OpenClaw resolves the canonical form automatically.
MoonshotAI. Your single `AI_GATEWAY_API_KEY` handles authentication for all
upstream providers.
</Accordion>
<Accordion title="Thinking levels">
`/think` options follow trusted upstream model prefixes when OpenClaw knows
the upstream provider contract. `vercel-ai-gateway/anthropic/...` uses the
Claude thinking profile, including adaptive defaults for Claude 4.6 models.
`vercel-ai-gateway/openai/gpt-5.4`, `gpt-5.5`, and Codex-style refs expose
`/think xhigh` just like the direct OpenAI/OpenAI Codex providers. Other
namespaced refs keep the normal reasoning levels unless their catalog
metadata declares more.
</Accordion>
</AccordionGroup>
## Related

View File

@@ -4,6 +4,7 @@ import {
buildStaticVercelAiGatewayProvider,
buildVercelAiGatewayProvider,
} from "./provider-catalog.js";
import { resolveVercelAiGatewayThinkingProfile } from "./thinking.js";
const PROVIDER_ID = "vercel-ai-gateway";
@@ -35,5 +36,6 @@ export default defineSingleProviderPluginEntry({
buildProvider: buildVercelAiGatewayProvider,
buildStaticProvider: buildStaticVercelAiGatewayProvider,
},
resolveThinkingProfile: ({ modelId }) => resolveVercelAiGatewayThinkingProfile(modelId),
},
});

View File

@@ -0,0 +1,72 @@
import {
registerProviderPlugin,
requireRegisteredProvider,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
describe("vercel ai gateway thinking profile", () => {
async function getProvider() {
const { providers } = await registerProviderPlugin({
plugin,
id: "vercel-ai-gateway",
name: "Vercel AI Gateway Provider",
});
return requireRegisteredProvider(providers, "vercel-ai-gateway");
}
it("exposes xhigh for trusted OpenAI upstream refs", async () => {
const provider = await getProvider();
const profile = provider.resolveThinkingProfile?.({
provider: "vercel-ai-gateway",
modelId: "openai/gpt-5.4",
});
expect(profile?.levels).toEqual(expect.arrayContaining([{ id: "xhigh" }]));
});
it("exposes Codex xhigh through the OpenAI upstream prefix", async () => {
const provider = await getProvider();
const profile = provider.resolveThinkingProfile?.({
provider: "vercel-ai-gateway",
modelId: "openai/gpt-5.3-codex-spark",
});
expect(profile?.levels).toEqual(expect.arrayContaining([{ id: "xhigh" }]));
});
it("reuses Claude thinking defaults for trusted Anthropic upstream refs", async () => {
const provider = await getProvider();
const profile = provider.resolveThinkingProfile?.({
provider: "vercel-ai-gateway",
modelId: "anthropic/claude-opus-4.6",
});
expect(profile).toMatchObject({
levels: expect.arrayContaining([{ id: "adaptive" }]),
defaultLevel: "adaptive",
});
expect(profile?.levels.some((level) => level.id === "xhigh" || level.id === "max")).toBe(false);
});
it("falls through for unsupported OpenAI or untrusted namespaced refs", async () => {
const provider = await getProvider();
const resolveThinkingProfile = provider.resolveThinkingProfile!;
expect(
resolveThinkingProfile({
provider: "vercel-ai-gateway",
modelId: "openai/gpt-4.1",
}),
).toBeUndefined();
expect(
resolveThinkingProfile({
provider: "vercel-ai-gateway",
modelId: "acme/gpt-5.4",
}),
).toBeUndefined();
});
});

View File

@@ -0,0 +1,77 @@
import type { ProviderThinkingProfile } from "openclaw/plugin-sdk/core";
import {
matchesExactOrPrefix,
resolveClaudeThinkingProfile,
} from "openclaw/plugin-sdk/provider-model-shared";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
const UPSTREAM_OPENAI_PREFIX = "openai/";
const UPSTREAM_ANTHROPIC_PREFIX = "anthropic/";
const BASE_OPENAI_THINKING_LEVELS = [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
] as const satisfies ProviderThinkingProfile["levels"];
const VERCEL_OPENAI_XHIGH_MODEL_IDS = [
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.4-pro",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.3-codex",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1-codex",
] as const;
function stripTrustedUpstreamPrefix(modelId: string, prefix: string): string | null {
const normalized = normalizeLowercaseStringOrEmpty(modelId);
if (!normalized.startsWith(prefix)) {
return null;
}
const upstreamModelId = normalized.slice(prefix.length).trim();
return upstreamModelId || null;
}
function resolveOpenAiThinkingProfile(modelId: string): ProviderThinkingProfile | undefined {
if (!matchesExactOrPrefix(modelId, VERCEL_OPENAI_XHIGH_MODEL_IDS)) {
return undefined;
}
return {
levels: [...BASE_OPENAI_THINKING_LEVELS, { id: "xhigh" }],
};
}
function hasVercelSpecificClaudeProfile(profile: ProviderThinkingProfile): boolean {
return Boolean(
profile.defaultLevel ||
profile.levels.some(
(level) => level.id === "adaptive" || level.id === "xhigh" || level.id === "max",
),
);
}
export function resolveVercelAiGatewayThinkingProfile(
modelId: string,
): ProviderThinkingProfile | undefined {
const openAiModelId = stripTrustedUpstreamPrefix(modelId, UPSTREAM_OPENAI_PREFIX);
if (openAiModelId) {
return resolveOpenAiThinkingProfile(openAiModelId);
}
const anthropicModelId = stripTrustedUpstreamPrefix(modelId, UPSTREAM_ANTHROPIC_PREFIX);
if (anthropicModelId) {
const profile = resolveClaudeThinkingProfile(anthropicModelId);
// Returning a base-only provider profile would hide catalog compat metadata
// from generic thinking resolution. Only take over when Claude has an
// upstream-specific default or elevated level set.
return hasVercelSpecificClaudeProfile(profile) ? profile : undefined;
}
return undefined;
}