fix(vercel-ai-gateway): resolve dynamic model selections (#95710)

Merged via squash.

Prepared head SHA: 0f136acbb3
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
Vincent Koc
2026-06-22 18:26:10 +08:00
committed by GitHub
parent b8b2f5d98f
commit c67bb1c5aa
5 changed files with 112 additions and 0 deletions
@@ -0,0 +1,22 @@
// Vercel Ai Gateway tests cover provider runtime hooks.
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
describe("vercel ai gateway provider hooks", () => {
it("resolves live-only model ids for the embedded runner", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const model = provider.resolveDynamicModel?.({
provider: "vercel-ai-gateway",
modelId: "custom/provider-model",
modelRegistry: { find: () => null },
} as never);
expect(model).toMatchObject({
id: "custom/provider-model",
provider: "vercel-ai-gateway",
api: "anthropic-messages",
baseUrl: "https://ai-gateway.vercel.sh",
});
});
});
+2
View File
@@ -4,6 +4,7 @@ import { applyVercelAiGatewayConfig, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from
import {
buildStaticVercelAiGatewayProvider,
buildVercelAiGatewayProvider,
resolveVercelAiGatewayModel,
} from "./provider-catalog.js";
import { resolveVercelAiGatewayThinkingProfile } from "./thinking.js";
@@ -37,6 +38,7 @@ export default defineSingleProviderPluginEntry({
buildProvider: buildVercelAiGatewayProvider,
buildStaticProvider: buildStaticVercelAiGatewayProvider,
},
resolveDynamicModel: ({ modelId }) => resolveVercelAiGatewayModel(modelId),
resolveThinkingProfile: ({ modelId }) => resolveVercelAiGatewayThinkingProfile(modelId),
},
});
+15
View File
@@ -142,6 +142,21 @@ function getStaticFallbackModel(id: string): ModelDefinitionConfig | undefined {
return fallback ? buildStaticModelDefinition(fallback) : undefined;
}
/** Builds runtime metadata for models returned by the live gateway catalog. */
export function resolveVercelAiGatewayDynamicModel(modelId: string): ModelDefinitionConfig {
return (
getStaticFallbackModel(modelId) ?? {
id: modelId,
name: modelId,
reasoning: false,
input: ["text"],
contextWindow: VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
maxTokens: VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS,
cost: VERCEL_AI_GATEWAY_DEFAULT_COST,
}
);
}
export function getStaticVercelAiGatewayModelCatalog(): ModelDefinitionConfig[] {
return STATIC_VERCEL_AI_GATEWAY_MODEL_CATALOG.map(buildStaticModelDefinition);
}
@@ -20,9 +20,11 @@ import {
VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS,
} from "./api.js";
import { resolveVercelAiGatewayDynamicModel } from "./models.js";
import {
buildStaticVercelAiGatewayProvider,
buildVercelAiGatewayProvider,
resolveVercelAiGatewayModel,
} from "./provider-catalog.js";
const STATIC_MODEL_IDS = [
@@ -83,6 +85,42 @@ describe("vercel ai gateway provider catalog", () => {
});
});
it("builds runtime metadata for live-only model ids", () => {
expect(resolveVercelAiGatewayDynamicModel("custom/provider-model")).toEqual({
id: "custom/provider-model",
name: "custom/provider-model",
reasoning: false,
input: ["text"],
contextWindow: VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
maxTokens: VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
});
});
it("adds transport metadata for runtime model resolution", () => {
expect(resolveVercelAiGatewayModel("custom/provider-model")).toMatchObject({
id: "custom/provider-model",
provider: "vercel-ai-gateway",
api: "anthropic-messages",
baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
});
});
it("preserves provider thinking metadata for known live-only upstream models", () => {
expect(resolveVercelAiGatewayModel("openai/gpt-5.5")).toMatchObject({
reasoning: true,
input: ["text", "image"],
});
expect(resolveVercelAiGatewayModel("anthropic/claude-sonnet-4-6")).toMatchObject({
input: ["text", "image"],
});
});
it("falls back to the static catalog for malformed successful model list payloads", async () => {
for (const payload of [[], { data: {} }, { data: [null] }]) {
clearLiveCatalogCacheForTests();
@@ -3,8 +3,43 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-sha
import {
discoverVercelAiGatewayModels,
getStaticVercelAiGatewayModelCatalog,
resolveVercelAiGatewayDynamicModel,
VERCEL_AI_GATEWAY_BASE_URL,
VERCEL_AI_GATEWAY_PROVIDER_ID,
} from "./models.js";
import { resolveVercelAiGatewayThinkingProfile } from "./thinking.js";
const VERCEL_AI_GATEWAY_IMAGE_MODEL_IDS = new Set([
"openai/gpt-5.5",
"openai/gpt-5.5-pro",
"openai/gpt-5.4",
"openai/gpt-5.4-pro",
"openai/gpt-5.4-mini",
"openai/gpt-5.4-nano",
"openai/gpt-5.3-codex",
"openai/gpt-5.3-codex-spark",
"openai/gpt-5.2",
"openai/gpt-5.2-codex",
"openai/gpt-5.1-codex",
]);
export function resolveVercelAiGatewayModel(modelId: string) {
const model = resolveVercelAiGatewayDynamicModel(modelId);
const input: Array<"text" | "image"> = model.input.includes("image")
? ["text", "image"]
: VERCEL_AI_GATEWAY_IMAGE_MODEL_IDS.has(modelId) ||
/^anthropic\/claude-(?:opus|sonnet|haiku)-/.test(modelId)
? ["text", "image"]
: ["text"];
return {
...model,
reasoning: model.reasoning || Boolean(resolveVercelAiGatewayThinkingProfile(modelId)),
input,
api: "anthropic-messages" as const,
provider: VERCEL_AI_GATEWAY_PROVIDER_ID,
baseUrl: VERCEL_AI_GATEWAY_BASE_URL,
};
}
export function buildStaticVercelAiGatewayProvider(): ModelProviderConfig {
return {