mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user