
























@@ -0,0 +1,199 @@
1+import { AnthropicVertex } from "@anthropic-ai/vertex-sdk";
2+import type { StreamFn } from "@mariozechner/pi-agent-core";
3+import { streamAnthropic, type AnthropicOptions, type Model } from "@mariozechner/pi-ai";
4+import {
5+applyAnthropicPayloadPolicyToParams,
6+resolveAnthropicPayloadPolicy,
7+} from "openclaw/plugin-sdk/provider-stream-shared";
8+import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexProjectId } from "./region.js";
9+10+type AnthropicVertexEffort = NonNullable<AnthropicOptions["effort"]>;
11+type AnthropicVertexAdaptiveEffort = AnthropicVertexEffort | "xhigh";
12+13+function isClaudeOpus47Model(modelId: string): boolean {
14+return modelId.includes("opus-4-7") || modelId.includes("opus-4.7");
15+}
16+17+function isClaudeOpus46Model(modelId: string): boolean {
18+return modelId.includes("opus-4-6") || modelId.includes("opus-4.6");
19+}
20+21+function supportsAdaptiveThinking(modelId: string): boolean {
22+return (
23+isClaudeOpus47Model(modelId) ||
24+isClaudeOpus46Model(modelId) ||
25+modelId.includes("sonnet-4-6") ||
26+modelId.includes("sonnet-4.6")
27+);
28+}
29+30+function mapAnthropicAdaptiveEffort(
31+reasoning: string,
32+modelId: string,
33+): AnthropicVertexAdaptiveEffort {
34+const effortMap: Record<string, AnthropicVertexAdaptiveEffort> = {
35+minimal: "low",
36+low: "low",
37+medium: "medium",
38+high: "high",
39+xhigh: isClaudeOpus47Model(modelId) ? "xhigh" : isClaudeOpus46Model(modelId) ? "max" : "high",
40+};
41+return effortMap[reasoning] ?? "high";
42+}
43+44+function resolveAnthropicVertexMaxTokens(params: {
45+modelMaxTokens: number | undefined;
46+requestedMaxTokens: number | undefined;
47+}): number | undefined {
48+const modelMax =
49+typeof params.modelMaxTokens === "number" &&
50+Number.isFinite(params.modelMaxTokens) &&
51+params.modelMaxTokens > 0
52+ ? Math.floor(params.modelMaxTokens)
53+ : undefined;
54+const requested =
55+typeof params.requestedMaxTokens === "number" &&
56+Number.isFinite(params.requestedMaxTokens) &&
57+params.requestedMaxTokens > 0
58+ ? Math.floor(params.requestedMaxTokens)
59+ : undefined;
60+61+if (modelMax !== undefined && requested !== undefined) {
62+return Math.min(requested, modelMax);
63+}
64+return requested ?? modelMax;
65+}
66+67+function createAnthropicVertexOnPayload(params: {
68+model: { api: string; baseUrl?: string; provider: string };
69+cacheRetention: AnthropicOptions["cacheRetention"] | undefined;
70+onPayload: AnthropicOptions["onPayload"] | undefined;
71+}): NonNullable<AnthropicOptions["onPayload"]> {
72+const policy = resolveAnthropicPayloadPolicy({
73+provider: params.model.provider,
74+api: params.model.api,
75+baseUrl: params.model.baseUrl,
76+cacheRetention: params.cacheRetention,
77+enableCacheControl: true,
78+});
79+80+function applyPolicy(payload: unknown): unknown {
81+if (payload && typeof payload === "object" && !Array.isArray(payload)) {
82+applyAnthropicPayloadPolicyToParams(payload as Record<string, unknown>, policy);
83+}
84+return payload;
85+}
86+87+return async (payload, model) => {
88+const shapedPayload = applyPolicy(payload);
89+const nextPayload = await params.onPayload?.(shapedPayload, model);
90+if (nextPayload === undefined || nextPayload === shapedPayload) {
91+return shapedPayload;
92+}
93+return applyPolicy(nextPayload);
94+};
95+}
96+97+/**
98+ * Create a StreamFn that routes through pi-ai's `streamAnthropic` with an
99+ * injected `AnthropicVertex` client. All streaming, message conversion, and
100+ * event handling is handled by pi-ai — we only supply the GCP-authenticated
101+ * client and map SimpleStreamOptions → AnthropicOptions.
102+ */
103+export function createAnthropicVertexStreamFn(
104+projectId: string | undefined,
105+region: string,
106+baseURL?: string,
107+): StreamFn {
108+const client = new AnthropicVertex({
109+ region,
110+ ...(baseURL ? { baseURL } : {}),
111+ ...(projectId ? { projectId } : {}),
112+});
113+114+return (model, context, options) => {
115+const transportModel = model as Model<"anthropic-messages"> & {
116+api: string;
117+baseUrl?: string;
118+provider: string;
119+};
120+const maxTokens = resolveAnthropicVertexMaxTokens({
121+modelMaxTokens: transportModel.maxTokens,
122+requestedMaxTokens: options?.maxTokens,
123+});
124+const opts: AnthropicOptions = {
125+client: client as unknown as AnthropicOptions["client"],
126+temperature: options?.temperature,
127+ ...(maxTokens !== undefined ? { maxTokens } : {}),
128+signal: options?.signal,
129+cacheRetention: options?.cacheRetention,
130+sessionId: options?.sessionId,
131+headers: options?.headers,
132+onPayload: createAnthropicVertexOnPayload({
133+model: transportModel,
134+cacheRetention: options?.cacheRetention,
135+onPayload: options?.onPayload,
136+}),
137+maxRetryDelayMs: options?.maxRetryDelayMs,
138+metadata: options?.metadata,
139+};
140+141+if (options?.reasoning) {
142+if (supportsAdaptiveThinking(model.id)) {
143+opts.thinkingEnabled = true;
144+opts.effort = mapAnthropicAdaptiveEffort(
145+options.reasoning,
146+model.id,
147+) as AnthropicVertexEffort;
148+} else {
149+opts.thinkingEnabled = true;
150+const budgets = options.thinkingBudgets;
151+opts.thinkingBudgetTokens =
152+(budgets && options.reasoning in budgets
153+ ? budgets[options.reasoning as keyof typeof budgets]
154+ : undefined) ?? 10000;
155+}
156+} else {
157+opts.thinkingEnabled = false;
158+}
159+160+return streamAnthropic(transportModel, context, opts);
161+};
162+}
163+164+function resolveAnthropicVertexSdkBaseUrl(baseUrl?: string): string | undefined {
165+const trimmed = baseUrl?.trim();
166+if (!trimmed) {
167+return undefined;
168+}
169+170+try {
171+const url = new URL(trimmed);
172+const normalizedPath = url.pathname.replace(/\/+$/, "");
173+if (!normalizedPath || normalizedPath === "") {
174+url.pathname = "/v1";
175+return url.toString().replace(/\/$/, "");
176+}
177+if (!normalizedPath.endsWith("/v1")) {
178+url.pathname = `${normalizedPath}/v1`;
179+return url.toString().replace(/\/$/, "");
180+}
181+return trimmed;
182+} catch {
183+return trimmed;
184+}
185+}
186+187+export function createAnthropicVertexStreamFnForModel(
188+model: { baseUrl?: string },
189+env: NodeJS.ProcessEnv = process.env,
190+): StreamFn {
191+return createAnthropicVertexStreamFn(
192+resolveAnthropicVertexProjectId(env),
193+resolveAnthropicVertexClientRegion({
194+baseUrl: model.baseUrl,
195+ env,
196+}),
197+resolveAnthropicVertexSdkBaseUrl(model.baseUrl),
198+);
199+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。