diff --git a/CHANGELOG.md b/CHANGELOG.md index f24bdb803b2..a7e6e31e10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - Plugin hooks: expose first-class run, message, sender, session, and trace correlation fields on message hook contexts and run lifecycle events. Thanks @vincentkoc. - TUI/dependencies: remove direct `cli-highlight` usage from the OpenClaw TUI code-block renderer, keeping themed code coloring without the extra root dependency. Thanks @vincentkoc. - Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc. +- Providers/Anthropic Vertex: move the Vertex SDK runtime behind the bundled provider plugin so core no longer owns that provider-specific dependency. Thanks @vincentkoc. - Plugins/activation: expose activation plan reasons and a richer plan API so callers can inspect why a plugin was selected while preserving existing id-list activation behavior. (#70943) Thanks @vincentkoc. - Plugins/source metadata: expose normalized install-source facts on provider and channel catalogs so onboarding can explain npm pinning, integrity state, and local availability before runtime loads. (#70951) Thanks @vincentkoc. - Plugins/catalog: pin the official external WeCom channel source to an exact npm release plus dist integrity, with a guard that official external sources stay integrity-pinned. (#70997) Thanks @vincentkoc. diff --git a/extensions/anthropic-vertex/api.test.ts b/extensions/anthropic-vertex/api.test.ts new file mode 100644 index 00000000000..d177f8833ed --- /dev/null +++ b/extensions/anthropic-vertex/api.test.ts @@ -0,0 +1,77 @@ +import type { Model } from "@mariozechner/pi-ai"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const hoisted = vi.hoisted(() => { + const streamAnthropicMock = vi.fn(() => Symbol("anthropic-vertex-stream")); + const anthropicVertexCtorMock = vi.fn(); + + return { + streamAnthropicMock, + anthropicVertexCtorMock, + }; +}); + +vi.mock("@mariozechner/pi-ai", async () => { + const original = + await vi.importActual("@mariozechner/pi-ai"); + return { + ...original, + streamAnthropic: hoisted.streamAnthropicMock, + }; +}); + +vi.mock("@anthropic-ai/vertex-sdk", () => ({ + AnthropicVertex: vi.fn(function MockAnthropicVertex(options: unknown) { + hoisted.anthropicVertexCtorMock(options); + return { options }; + }), +})); + +let createAnthropicVertexStreamFn: typeof import("./api.js").createAnthropicVertexStreamFn; +let createAnthropicVertexStreamFnForModel: typeof import("./api.js").createAnthropicVertexStreamFnForModel; + +function makeModel(): Model<"anthropic-messages"> { + return { + id: "claude-sonnet-4-6", + api: "anthropic-messages", + provider: "anthropic-vertex", + maxTokens: 128000, + } as Model<"anthropic-messages">; +} + +describe("Anthropic Vertex API stream factories", () => { + beforeAll(async () => { + ({ createAnthropicVertexStreamFn, createAnthropicVertexStreamFnForModel } = + await import("./api.js")); + }); + + beforeEach(() => { + hoisted.streamAnthropicMock.mockClear(); + hoisted.anthropicVertexCtorMock.mockClear(); + }); + + it("reuses the runtime stream factory across direct stream calls", async () => { + const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5"); + const model = makeModel(); + + await streamFn(model, { messages: [] }, {}); + await streamFn(model, { messages: [] }, {}); + + expect(hoisted.anthropicVertexCtorMock).toHaveBeenCalledTimes(1); + expect(hoisted.streamAnthropicMock).toHaveBeenCalledTimes(2); + }); + + it("reuses the runtime stream factory across model-derived stream calls", async () => { + const streamFn = createAnthropicVertexStreamFnForModel(makeModel(), { + ANTHROPIC_VERTEX_PROJECT_ID: "vertex-project", + GOOGLE_CLOUD_LOCATION: "us-east5", + } as NodeJS.ProcessEnv); + const model = makeModel(); + + await streamFn(model, { messages: [] }, {}); + await streamFn(model, { messages: [] }, {}); + + expect(hoisted.anthropicVertexCtorMock).toHaveBeenCalledTimes(1); + expect(hoisted.streamAnthropicMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/extensions/anthropic-vertex/api.ts b/extensions/anthropic-vertex/api.ts index 93d04d19d85..e44b82b045a 100644 --- a/extensions/anthropic-vertex/api.ts +++ b/extensions/anthropic-vertex/api.ts @@ -1,3 +1,5 @@ +import type { StreamFn } from "@mariozechner/pi-agent-core"; + export { ANTHROPIC_VERTEX_DEFAULT_MODEL_ID, buildAnthropicVertexProvider, @@ -40,3 +42,30 @@ export function resolveImplicitAnthropicVertexProvider(params?: { env?: NodeJS.P return buildAnthropicVertexProvider({ env }); } + +export function createAnthropicVertexStreamFn( + projectId: string | undefined, + region: string, + baseURL?: string, +): StreamFn { + const streamFnPromise = import("./stream-runtime.js").then((runtime) => + runtime.createAnthropicVertexStreamFn(projectId, region, baseURL), + ); + return async (model, context, options) => { + const streamFn = await streamFnPromise; + return streamFn(model, context, options); + }; +} + +export function createAnthropicVertexStreamFnForModel( + model: { baseUrl?: string }, + env: NodeJS.ProcessEnv = process.env, +): StreamFn { + const streamFnPromise = import("./stream-runtime.js").then((runtime) => + runtime.createAnthropicVertexStreamFnForModel(model, env), + ); + return async (...args) => { + const streamFn = await streamFnPromise; + return streamFn(...args); + }; +} diff --git a/extensions/anthropic-vertex/package.json b/extensions/anthropic-vertex/package.json index 1992c452c1f..ae001972279 100644 --- a/extensions/anthropic-vertex/package.json +++ b/extensions/anthropic-vertex/package.json @@ -4,10 +4,18 @@ "private": true, "description": "OpenClaw Anthropic Vertex provider plugin", "type": "module", + "dependencies": { + "@anthropic-ai/vertex-sdk": "^0.16.0", + "@mariozechner/pi-agent-core": "0.70.2", + "@mariozechner/pi-ai": "0.70.2" + }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" }, "openclaw": { + "bundle": { + "stageRuntimeDependencies": true + }, "extensions": [ "./index.ts" ] diff --git a/src/agents/anthropic-vertex-stream.test.ts b/extensions/anthropic-vertex/stream-runtime.test.ts similarity index 89% rename from src/agents/anthropic-vertex-stream.test.ts rename to extensions/anthropic-vertex/stream-runtime.test.ts index ef10048cf9a..bddf6e4ccf4 100644 --- a/src/agents/anthropic-vertex-stream.test.ts +++ b/extensions/anthropic-vertex/stream-runtime.test.ts @@ -1,6 +1,7 @@ import type { Model } from "@mariozechner/pi-ai"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js"; + +const SYSTEM_PROMPT_CACHE_BOUNDARY = "\n\n"; const hoisted = vi.hoisted(() => { const streamAnthropicMock = vi.fn<(model: unknown, context: unknown, options: unknown) => symbol>( @@ -31,28 +32,8 @@ vi.mock("@anthropic-ai/vertex-sdk", () => ({ }), })); -vi.mock("../plugin-sdk/anthropic-vertex.js", () => ({ - resolveAnthropicVertexProjectId: (env: NodeJS.ProcessEnv = process.env) => - env.ANTHROPIC_VERTEX_PROJECT_ID || env.GOOGLE_CLOUD_PROJECT || env.GOOGLE_CLOUD_PROJECT_ID, - resolveAnthropicVertexClientRegion: (params?: { baseUrl?: string; env?: NodeJS.ProcessEnv }) => { - const baseUrl = params?.baseUrl?.trim(); - if (baseUrl) { - try { - const host = new URL(baseUrl).hostname; - const match = /^([a-z0-9-]+)-aiplatform\.googleapis\.com$/u.exec(host); - if (match?.[1]) { - return match[1]; - } - } catch { - // noop; test seam only - } - } - return params?.env?.GOOGLE_CLOUD_LOCATION || params?.env?.CLOUD_ML_REGION || "global"; - }, -})); - -let createAnthropicVertexStreamFn: typeof import("./anthropic-vertex-stream.js").createAnthropicVertexStreamFn; -let createAnthropicVertexStreamFnForModel: typeof import("./anthropic-vertex-stream.js").createAnthropicVertexStreamFnForModel; +let createAnthropicVertexStreamFn: typeof import("./stream-runtime.js").createAnthropicVertexStreamFn; +let createAnthropicVertexStreamFnForModel: typeof import("./stream-runtime.js").createAnthropicVertexStreamFnForModel; function makeModel(params: { id: string; maxTokens?: number }): Model<"anthropic-messages"> { return { @@ -121,7 +102,7 @@ function buildExpectedCacheBoundaryPayload(messageText: string) { describe("createAnthropicVertexStreamFn", () => { beforeAll(async () => { ({ createAnthropicVertexStreamFn, createAnthropicVertexStreamFnForModel } = - await import("./anthropic-vertex-stream.js")); + await import("./stream-runtime.js")); }); beforeEach(() => { diff --git a/extensions/anthropic-vertex/stream-runtime.ts b/extensions/anthropic-vertex/stream-runtime.ts new file mode 100644 index 00000000000..f399cc35ae1 --- /dev/null +++ b/extensions/anthropic-vertex/stream-runtime.ts @@ -0,0 +1,199 @@ +import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; +import type { StreamFn } from "@mariozechner/pi-agent-core"; +import { streamAnthropic, type AnthropicOptions, type Model } from "@mariozechner/pi-ai"; +import { + applyAnthropicPayloadPolicyToParams, + resolveAnthropicPayloadPolicy, +} from "openclaw/plugin-sdk/provider-stream-shared"; +import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexProjectId } from "./region.js"; + +type AnthropicVertexEffort = NonNullable; +type AnthropicVertexAdaptiveEffort = AnthropicVertexEffort | "xhigh"; + +function isClaudeOpus47Model(modelId: string): boolean { + return modelId.includes("opus-4-7") || modelId.includes("opus-4.7"); +} + +function isClaudeOpus46Model(modelId: string): boolean { + return modelId.includes("opus-4-6") || modelId.includes("opus-4.6"); +} + +function supportsAdaptiveThinking(modelId: string): boolean { + return ( + isClaudeOpus47Model(modelId) || + isClaudeOpus46Model(modelId) || + modelId.includes("sonnet-4-6") || + modelId.includes("sonnet-4.6") + ); +} + +function mapAnthropicAdaptiveEffort( + reasoning: string, + modelId: string, +): AnthropicVertexAdaptiveEffort { + const effortMap: Record = { + minimal: "low", + low: "low", + medium: "medium", + high: "high", + xhigh: isClaudeOpus47Model(modelId) ? "xhigh" : isClaudeOpus46Model(modelId) ? "max" : "high", + }; + return effortMap[reasoning] ?? "high"; +} + +function resolveAnthropicVertexMaxTokens(params: { + modelMaxTokens: number | undefined; + requestedMaxTokens: number | undefined; +}): number | undefined { + const modelMax = + typeof params.modelMaxTokens === "number" && + Number.isFinite(params.modelMaxTokens) && + params.modelMaxTokens > 0 + ? Math.floor(params.modelMaxTokens) + : undefined; + const requested = + typeof params.requestedMaxTokens === "number" && + Number.isFinite(params.requestedMaxTokens) && + params.requestedMaxTokens > 0 + ? Math.floor(params.requestedMaxTokens) + : undefined; + + if (modelMax !== undefined && requested !== undefined) { + return Math.min(requested, modelMax); + } + return requested ?? modelMax; +} + +function createAnthropicVertexOnPayload(params: { + model: { api: string; baseUrl?: string; provider: string }; + cacheRetention: AnthropicOptions["cacheRetention"] | undefined; + onPayload: AnthropicOptions["onPayload"] | undefined; +}): NonNullable { + const policy = resolveAnthropicPayloadPolicy({ + provider: params.model.provider, + api: params.model.api, + baseUrl: params.model.baseUrl, + cacheRetention: params.cacheRetention, + enableCacheControl: true, + }); + + function applyPolicy(payload: unknown): unknown { + if (payload && typeof payload === "object" && !Array.isArray(payload)) { + applyAnthropicPayloadPolicyToParams(payload as Record, policy); + } + return payload; + } + + return async (payload, model) => { + const shapedPayload = applyPolicy(payload); + const nextPayload = await params.onPayload?.(shapedPayload, model); + if (nextPayload === undefined || nextPayload === shapedPayload) { + return shapedPayload; + } + return applyPolicy(nextPayload); + }; +} + +/** + * Create a StreamFn that routes through pi-ai's `streamAnthropic` with an + * injected `AnthropicVertex` client. All streaming, message conversion, and + * event handling is handled by pi-ai — we only supply the GCP-authenticated + * client and map SimpleStreamOptions → AnthropicOptions. + */ +export function createAnthropicVertexStreamFn( + projectId: string | undefined, + region: string, + baseURL?: string, +): StreamFn { + const client = new AnthropicVertex({ + region, + ...(baseURL ? { baseURL } : {}), + ...(projectId ? { projectId } : {}), + }); + + return (model, context, options) => { + const transportModel = model as Model<"anthropic-messages"> & { + api: string; + baseUrl?: string; + provider: string; + }; + const maxTokens = resolveAnthropicVertexMaxTokens({ + modelMaxTokens: transportModel.maxTokens, + requestedMaxTokens: options?.maxTokens, + }); + const opts: AnthropicOptions = { + client: client as unknown as AnthropicOptions["client"], + temperature: options?.temperature, + ...(maxTokens !== undefined ? { maxTokens } : {}), + signal: options?.signal, + cacheRetention: options?.cacheRetention, + sessionId: options?.sessionId, + headers: options?.headers, + onPayload: createAnthropicVertexOnPayload({ + model: transportModel, + cacheRetention: options?.cacheRetention, + onPayload: options?.onPayload, + }), + maxRetryDelayMs: options?.maxRetryDelayMs, + metadata: options?.metadata, + }; + + if (options?.reasoning) { + if (supportsAdaptiveThinking(model.id)) { + opts.thinkingEnabled = true; + opts.effort = mapAnthropicAdaptiveEffort( + options.reasoning, + model.id, + ) as AnthropicVertexEffort; + } else { + opts.thinkingEnabled = true; + const budgets = options.thinkingBudgets; + opts.thinkingBudgetTokens = + (budgets && options.reasoning in budgets + ? budgets[options.reasoning as keyof typeof budgets] + : undefined) ?? 10000; + } + } else { + opts.thinkingEnabled = false; + } + + return streamAnthropic(transportModel, context, opts); + }; +} + +function resolveAnthropicVertexSdkBaseUrl(baseUrl?: string): string | undefined { + const trimmed = baseUrl?.trim(); + if (!trimmed) { + return undefined; + } + + try { + const url = new URL(trimmed); + const normalizedPath = url.pathname.replace(/\/+$/, ""); + if (!normalizedPath || normalizedPath === "") { + url.pathname = "/v1"; + return url.toString().replace(/\/$/, ""); + } + if (!normalizedPath.endsWith("/v1")) { + url.pathname = `${normalizedPath}/v1`; + return url.toString().replace(/\/$/, ""); + } + return trimmed; + } catch { + return trimmed; + } +} + +export function createAnthropicVertexStreamFnForModel( + model: { baseUrl?: string }, + env: NodeJS.ProcessEnv = process.env, +): StreamFn { + return createAnthropicVertexStreamFn( + resolveAnthropicVertexProjectId(env), + resolveAnthropicVertexClientRegion({ + baseUrl: model.baseUrl, + env, + }), + resolveAnthropicVertexSdkBaseUrl(model.baseUrl), + ); +} diff --git a/package.json b/package.json index 56cf2633643..96457d25948 100644 --- a/package.json +++ b/package.json @@ -1581,7 +1581,6 @@ }, "dependencies": { "@agentclientprotocol/sdk": "0.19.1", - "@anthropic-ai/vertex-sdk": "^0.16.0", "@clack/prompts": "^1.2.0", "@lydell/node-pty": "1.2.0-beta.12", "@mariozechner/pi-agent-core": "0.70.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 962e5a01238..bbac03c1ad7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,6 @@ importers: '@agentclientprotocol/sdk': specifier: 0.19.1 version: 0.19.1(zod@4.3.6) - '@anthropic-ai/vertex-sdk': - specifier: ^0.16.0 - version: 0.16.0(zod@4.3.6) '@clack/prompts': specifier: ^1.2.0 version: 1.2.0 @@ -292,6 +289,16 @@ importers: version: link:../../packages/plugin-sdk extensions/anthropic-vertex: + dependencies: + '@anthropic-ai/vertex-sdk': + specifier: ^0.16.0 + version: 0.16.0(zod@4.3.6) + '@mariozechner/pi-agent-core': + specifier: 0.70.2 + version: 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) + '@mariozechner/pi-ai': + specifier: 0.70.2 + version: 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) devDependencies: '@openclaw/plugin-sdk': specifier: workspace:* diff --git a/scripts/lib/dependency-ownership.json b/scripts/lib/dependency-ownership.json index fda625ed3c6..b214762ccff 100644 --- a/scripts/lib/dependency-ownership.json +++ b/scripts/lib/dependency-ownership.json @@ -6,11 +6,6 @@ "class": "core-runtime", "risk": ["protocol-client"] }, - "@anthropic-ai/vertex-sdk": { - "owner": "provider:anthropic-vertex", - "class": "default-runtime-initially", - "risk": ["provider-sdk"] - }, "@clack/prompts": { "owner": "core:cli", "class": "core-runtime", diff --git a/src/agents/anthropic-vertex-stream.ts b/src/agents/anthropic-vertex-stream.ts index 534ab8fca9d..a9e616c97d8 100644 --- a/src/agents/anthropic-vertex-stream.ts +++ b/src/agents/anthropic-vertex-stream.ts @@ -1,202 +1,40 @@ -import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; import type { StreamFn } from "@mariozechner/pi-agent-core"; -import { streamAnthropic, type AnthropicOptions, type Model } from "@mariozechner/pi-ai"; -import { - resolveAnthropicVertexClientRegion, - resolveAnthropicVertexProjectId, -} from "../plugin-sdk/anthropic-vertex.js"; -import { - applyAnthropicPayloadPolicyToParams, - resolveAnthropicPayloadPolicy, -} from "./anthropic-payload-policy.js"; +import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-loader.js"; -type AnthropicVertexEffort = NonNullable; -type AnthropicVertexAdaptiveEffort = AnthropicVertexEffort | "xhigh"; +type AnthropicVertexStreamFacade = { + createAnthropicVertexStreamFn: ( + projectId: string | undefined, + region: string, + baseURL?: string, + ) => StreamFn; + createAnthropicVertexStreamFnForModel: ( + model: { baseUrl?: string }, + env?: NodeJS.ProcessEnv, + ) => StreamFn; +}; -function isClaudeOpus47Model(modelId: string): boolean { - return modelId.includes("opus-4-7") || modelId.includes("opus-4.7"); -} - -function isClaudeOpus46Model(modelId: string): boolean { - return modelId.includes("opus-4-6") || modelId.includes("opus-4.6"); -} - -function supportsAdaptiveThinking(modelId: string): boolean { - return ( - isClaudeOpus47Model(modelId) || - isClaudeOpus46Model(modelId) || - modelId.includes("sonnet-4-6") || - modelId.includes("sonnet-4.6") - ); -} - -function mapAnthropicAdaptiveEffort( - reasoning: string, - modelId: string, -): AnthropicVertexAdaptiveEffort { - const effortMap: Record = { - minimal: "low", - low: "low", - medium: "medium", - high: "high", - xhigh: isClaudeOpus47Model(modelId) ? "xhigh" : isClaudeOpus46Model(modelId) ? "max" : "high", - }; - return effortMap[reasoning] ?? "high"; -} - -function resolveAnthropicVertexMaxTokens(params: { - modelMaxTokens: number | undefined; - requestedMaxTokens: number | undefined; -}): number | undefined { - const modelMax = - typeof params.modelMaxTokens === "number" && - Number.isFinite(params.modelMaxTokens) && - params.modelMaxTokens > 0 - ? Math.floor(params.modelMaxTokens) - : undefined; - const requested = - typeof params.requestedMaxTokens === "number" && - Number.isFinite(params.requestedMaxTokens) && - params.requestedMaxTokens > 0 - ? Math.floor(params.requestedMaxTokens) - : undefined; - - if (modelMax !== undefined && requested !== undefined) { - return Math.min(requested, modelMax); - } - return requested ?? modelMax; -} - -function createAnthropicVertexOnPayload(params: { - model: { api: string; baseUrl?: string; provider: string }; - cacheRetention: AnthropicOptions["cacheRetention"] | undefined; - onPayload: AnthropicOptions["onPayload"] | undefined; -}): NonNullable { - const policy = resolveAnthropicPayloadPolicy({ - provider: params.model.provider, - api: params.model.api, - baseUrl: params.model.baseUrl, - cacheRetention: params.cacheRetention, - enableCacheControl: true, +function loadAnthropicVertexStreamFacade(): AnthropicVertexStreamFacade { + return loadBundledPluginPublicSurfaceModuleSync({ + dirName: "anthropic-vertex", + artifactBasename: "api.js", }); - - function applyPolicy(payload: unknown): unknown { - if (payload && typeof payload === "object" && !Array.isArray(payload)) { - applyAnthropicPayloadPolicyToParams(payload as Record, policy); - } - return payload; - } - - return async (payload, model) => { - const shapedPayload = applyPolicy(payload); - const nextPayload = await params.onPayload?.(shapedPayload, model); - if (nextPayload === undefined || nextPayload === shapedPayload) { - return shapedPayload; - } - return applyPolicy(nextPayload); - }; } -/** - * Create a StreamFn that routes through pi-ai's `streamAnthropic` with an - * injected `AnthropicVertex` client. All streaming, message conversion, and - * event handling is handled by pi-ai — we only supply the GCP-authenticated - * client and map SimpleStreamOptions → AnthropicOptions. - */ export function createAnthropicVertexStreamFn( projectId: string | undefined, region: string, baseURL?: string, ): StreamFn { - const client = new AnthropicVertex({ + return loadAnthropicVertexStreamFacade().createAnthropicVertexStreamFn( + projectId, region, - ...(baseURL ? { baseURL } : {}), - ...(projectId ? { projectId } : {}), - }); - - return (model, context, options) => { - const transportModel = model as Model<"anthropic-messages"> & { - api: string; - baseUrl?: string; - provider: string; - }; - const maxTokens = resolveAnthropicVertexMaxTokens({ - modelMaxTokens: transportModel.maxTokens, - requestedMaxTokens: options?.maxTokens, - }); - const opts: AnthropicOptions = { - client: client as unknown as AnthropicOptions["client"], - temperature: options?.temperature, - ...(maxTokens !== undefined ? { maxTokens } : {}), - signal: options?.signal, - cacheRetention: options?.cacheRetention, - sessionId: options?.sessionId, - headers: options?.headers, - onPayload: createAnthropicVertexOnPayload({ - model: transportModel, - cacheRetention: options?.cacheRetention, - onPayload: options?.onPayload, - }), - maxRetryDelayMs: options?.maxRetryDelayMs, - metadata: options?.metadata, - }; - - if (options?.reasoning) { - if (supportsAdaptiveThinking(model.id)) { - opts.thinkingEnabled = true; - opts.effort = mapAnthropicAdaptiveEffort( - options.reasoning, - model.id, - ) as AnthropicVertexEffort; - } else { - opts.thinkingEnabled = true; - const budgets = options.thinkingBudgets; - opts.thinkingBudgetTokens = - (budgets && options.reasoning in budgets - ? budgets[options.reasoning as keyof typeof budgets] - : undefined) ?? 10000; - } - } else { - opts.thinkingEnabled = false; - } - - return streamAnthropic(transportModel, context, opts); - }; -} - -function resolveAnthropicVertexSdkBaseUrl(baseUrl?: string): string | undefined { - const trimmed = baseUrl?.trim(); - if (!trimmed) { - return undefined; - } - - try { - const url = new URL(trimmed); - const normalizedPath = url.pathname.replace(/\/+$/, ""); - if (!normalizedPath || normalizedPath === "") { - url.pathname = "/v1"; - return url.toString().replace(/\/$/, ""); - } - if (!normalizedPath.endsWith("/v1")) { - url.pathname = `${normalizedPath}/v1`; - return url.toString().replace(/\/$/, ""); - } - return trimmed; - } catch { - return trimmed; - } + baseURL, + ); } export function createAnthropicVertexStreamFnForModel( model: { baseUrl?: string }, env: NodeJS.ProcessEnv = process.env, ): StreamFn { - return createAnthropicVertexStreamFn( - resolveAnthropicVertexProjectId(env), - resolveAnthropicVertexClientRegion({ - baseUrl: model.baseUrl, - env, - }), - resolveAnthropicVertexSdkBaseUrl(model.baseUrl), - ); + return loadAnthropicVertexStreamFacade().createAnthropicVertexStreamFnForModel(model, env); }