Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
11
extensions/anthropic-vertex/README.md
Normal file
11
extensions/anthropic-vertex/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# OpenClaw Anthropic Vertex Provider
|
||||
|
||||
Official OpenClaw provider plugin for Claude models hosted through Google Vertex AI.
|
||||
|
||||
Install from OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw plugin add @openclaw/anthropic-vertex-provider
|
||||
```
|
||||
|
||||
Configure Google Cloud credentials and the target Vertex project/region in OpenClaw, then select Claude models with the Anthropic Vertex provider.
|
||||
78
extensions/anthropic-vertex/api.test.ts
Normal file
78
extensions/anthropic-vertex/api.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Anthropic Vertex tests cover api plugin behavior.
|
||||
import { createAssistantMessageEventStream, type Model } from "openclaw/plugin-sdk/llm";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
function createStreamDeps(): {
|
||||
deps: AnthropicVertexStreamDeps;
|
||||
streamAnthropicMock: ReturnType<typeof vi.fn>;
|
||||
anthropicVertexCtorMock: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const streamAnthropicMock = vi.fn(
|
||||
(..._args: Parameters<AnthropicVertexStreamDeps["streamAnthropic"]>) =>
|
||||
createAssistantMessageEventStream(),
|
||||
);
|
||||
const anthropicVertexCtorMock = vi.fn();
|
||||
const MockAnthropicVertex = function MockAnthropicVertex(options: unknown) {
|
||||
anthropicVertexCtorMock(options);
|
||||
} as unknown as AnthropicVertexStreamDeps["AnthropicVertex"];
|
||||
|
||||
return {
|
||||
deps: {
|
||||
AnthropicVertex: MockAnthropicVertex,
|
||||
streamAnthropic: streamAnthropicMock,
|
||||
},
|
||||
streamAnthropicMock,
|
||||
anthropicVertexCtorMock,
|
||||
};
|
||||
}
|
||||
|
||||
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"));
|
||||
});
|
||||
|
||||
it("reuses the runtime stream factory across direct stream calls", async () => {
|
||||
const { deps, streamAnthropicMock, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel();
|
||||
|
||||
await streamFn(model, { messages: [] }, {});
|
||||
await streamFn(model, { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledTimes(1);
|
||||
expect(streamAnthropicMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reuses the runtime stream factory across model-derived stream calls", async () => {
|
||||
const { deps, streamAnthropicMock, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFnForModel(
|
||||
makeModel(),
|
||||
{
|
||||
ANTHROPIC_VERTEX_PROJECT_ID: "vertex-project",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
} as NodeJS.ProcessEnv,
|
||||
deps,
|
||||
);
|
||||
const model = makeModel();
|
||||
|
||||
await streamFn(model, { messages: [] }, {});
|
||||
await streamFn(model, { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledTimes(1);
|
||||
expect(streamAnthropicMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
85
extensions/anthropic-vertex/api.ts
Normal file
85
extensions/anthropic-vertex/api.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Public Anthropic Vertex API barrel. It exposes lightweight discovery helpers
|
||||
* and lazy stream factories without eagerly importing the Vertex SDK runtime.
|
||||
*/
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
export {
|
||||
ANTHROPIC_VERTEX_DEFAULT_MODEL_ID,
|
||||
buildAnthropicVertexProvider,
|
||||
} from "./provider-catalog.js";
|
||||
export {
|
||||
hasAnthropicVertexAvailableAuth,
|
||||
hasAnthropicVertexCredentials,
|
||||
resolveAnthropicVertexClientRegion,
|
||||
resolveAnthropicVertexConfigApiKey,
|
||||
resolveAnthropicVertexProjectId,
|
||||
resolveAnthropicVertexRegion,
|
||||
resolveAnthropicVertexRegionFromBaseUrl,
|
||||
} from "./region.js";
|
||||
import { buildAnthropicVertexProvider } from "./provider-catalog.js";
|
||||
import { hasAnthropicVertexAvailableAuth } from "./region.js";
|
||||
|
||||
const loadStreamRuntimeModule = createLazyRuntimeModule(() => import("./stream-runtime.js"));
|
||||
|
||||
/** Merge an implicit Anthropic Vertex provider with explicit user config. */
|
||||
export function mergeImplicitAnthropicVertexProvider(params: {
|
||||
existing?: ReturnType<typeof buildAnthropicVertexProvider>;
|
||||
implicit: ReturnType<typeof buildAnthropicVertexProvider>;
|
||||
}) {
|
||||
const { existing, implicit } = params;
|
||||
if (!existing) {
|
||||
return implicit;
|
||||
}
|
||||
return {
|
||||
...implicit,
|
||||
...existing,
|
||||
models:
|
||||
Array.isArray(existing.models) && existing.models.length > 0
|
||||
? existing.models
|
||||
: implicit.models,
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve an implicit Anthropic Vertex provider when ADC credentials are available. */
|
||||
export function resolveImplicitAnthropicVertexProvider(params?: { env?: NodeJS.ProcessEnv }) {
|
||||
const env = params?.env ?? process.env;
|
||||
if (!hasAnthropicVertexAvailableAuth(env)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildAnthropicVertexProvider({ env });
|
||||
}
|
||||
|
||||
/** Create a lazy Anthropic Vertex stream function for a known project/region/base URL. */
|
||||
export function createAnthropicVertexStreamFn(
|
||||
projectId: string | undefined,
|
||||
region: string,
|
||||
baseURL?: string,
|
||||
deps?: AnthropicVertexStreamDeps,
|
||||
): StreamFn {
|
||||
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
|
||||
runtime.createAnthropicVertexStreamFn(projectId, region, baseURL, deps),
|
||||
);
|
||||
return async (model, context, options) => {
|
||||
const streamFn = await streamFnPromise;
|
||||
return streamFn(model, context, options);
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a lazy Anthropic Vertex stream function using model base URL and env hints. */
|
||||
export function createAnthropicVertexStreamFnForModel(
|
||||
model: { baseUrl?: string },
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
deps?: AnthropicVertexStreamDeps,
|
||||
): StreamFn {
|
||||
const streamFnPromise = loadStreamRuntimeModule().then((runtime) =>
|
||||
runtime.createAnthropicVertexStreamFnForModel(model, env, deps),
|
||||
);
|
||||
return async (...args) => {
|
||||
const streamFn = await streamFnPromise;
|
||||
return streamFn(...args);
|
||||
};
|
||||
}
|
||||
240
extensions/anthropic-vertex/index.test.ts
Normal file
240
extensions/anthropic-vertex/index.test.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
// Anthropic Vertex tests cover index plugin behavior.
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { hasAnthropicVertexAvailableAuthMock } = vi.hoisted(() => ({
|
||||
hasAnthropicVertexAvailableAuthMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./api.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./api.js")>();
|
||||
return {
|
||||
...actual,
|
||||
hasAnthropicVertexAvailableAuth: hasAnthropicVertexAvailableAuthMock,
|
||||
};
|
||||
});
|
||||
|
||||
import anthropicVertexPlugin from "./index.js";
|
||||
|
||||
describe("anthropic-vertex provider plugin", () => {
|
||||
beforeEach(() => {
|
||||
hasAnthropicVertexAvailableAuthMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.doUnmock("./api.js");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("resolves the ADC marker through the provider hook", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
expect(
|
||||
provider.resolveConfigApiKey?.({
|
||||
provider: "anthropic-vertex",
|
||||
env: {
|
||||
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
|
||||
} as NodeJS.ProcessEnv,
|
||||
} as never),
|
||||
).toBe("gcp-vertex-credentials");
|
||||
});
|
||||
|
||||
it("merges the implicit Vertex catalog into explicit provider overrides", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
const result = await provider.catalog?.run({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
"anthropic-vertex": {
|
||||
baseUrl: "https://europe-west4-aiplatform.googleapis.com",
|
||||
headers: { "x-test-header": "1" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
} as NodeJS.ProcessEnv,
|
||||
resolveProviderApiKey: () => ({ apiKey: undefined }),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: undefined,
|
||||
discoveryApiKey: undefined,
|
||||
mode: "none",
|
||||
source: "none",
|
||||
}),
|
||||
} as never);
|
||||
|
||||
if (!result || !("provider" in result)) {
|
||||
throw new Error("expected single provider catalog result");
|
||||
}
|
||||
expect(result.provider.api).toBe("anthropic-messages");
|
||||
expect(result.provider.apiKey).toBe("gcp-vertex-credentials");
|
||||
expect(result.provider.baseUrl).toBe("https://europe-west4-aiplatform.googleapis.com");
|
||||
expect(result.provider.headers).toEqual({ "x-test-header": "1" });
|
||||
expect(result.provider.models.map((model) => model.id)).toEqual([
|
||||
"claude-fable-5",
|
||||
"claude-opus-4-8",
|
||||
"claude-opus-4-6",
|
||||
"claude-sonnet-4-6",
|
||||
]);
|
||||
expect(result.provider.models[0]?.thinkingLevelMap).toEqual({
|
||||
off: "low",
|
||||
minimal: "low",
|
||||
xhigh: "xhigh",
|
||||
max: "max",
|
||||
});
|
||||
expect(result.provider.models[2]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" });
|
||||
expect(result.provider.models[3]?.thinkingLevelMap).toEqual({ xhigh: null, max: "max" });
|
||||
});
|
||||
|
||||
it("owns Anthropic-style replay policy", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
expect(
|
||||
provider.buildReplayPolicy?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelApi: "anthropic-messages",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
} as never),
|
||||
).toEqual({
|
||||
sanitizeMode: "full",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
preserveNativeAnthropicToolUseIds: true,
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
validateAnthropicTurns: true,
|
||||
allowSyntheticToolResults: true,
|
||||
});
|
||||
expect(
|
||||
provider.buildReplayPolicy?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelApi: "anthropic-messages",
|
||||
modelId: "claude-fable-5",
|
||||
} as never),
|
||||
).not.toHaveProperty("dropThinkingBlocks");
|
||||
});
|
||||
|
||||
it("owns Anthropic-style thinking policy", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
const opus48Profile = provider.resolveThinkingProfile?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-opus-4-8",
|
||||
} as never);
|
||||
|
||||
expect(opus48Profile?.defaultLevel).toBe("off");
|
||||
expect(opus48Profile?.levels.map((level) => level.id)).toContain("max");
|
||||
|
||||
const fableProfile = provider.resolveThinkingProfile?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-fable-5",
|
||||
} as never);
|
||||
expect(fableProfile?.defaultLevel).toBe("high");
|
||||
expect(fableProfile?.preserveWhenCatalogReasoningFalse).toBe(true);
|
||||
|
||||
const aliasProfile = provider.resolveThinkingProfile?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "production-claude",
|
||||
params: { canonicalModelId: "claude-fable-5" },
|
||||
} as never);
|
||||
expect(aliasProfile?.defaultLevel).toBe("high");
|
||||
});
|
||||
|
||||
it("restores Fable metadata for explicit Vertex catalog rows", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
const normalized = provider.normalizeResolvedModel?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-fable-5",
|
||||
model: {
|
||||
id: "claude-fable-5",
|
||||
name: "Claude Fable 5",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic-vertex",
|
||||
baseUrl: "https://aiplatform.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(normalized).toMatchObject({
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_000_000,
|
||||
contextTokens: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
thinkingLevelMap: {
|
||||
off: "low",
|
||||
minimal: "low",
|
||||
xhigh: "xhigh",
|
||||
max: "max",
|
||||
},
|
||||
});
|
||||
|
||||
const aliasNormalized = provider.normalizeResolvedModel?.({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "production-claude",
|
||||
model: {
|
||||
id: "production-claude",
|
||||
name: "Production Claude",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic-vertex",
|
||||
baseUrl: "https://aiplatform.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8192,
|
||||
params: { canonicalModelId: "claude-fable-5" },
|
||||
thinkingLevelMap: { max: null },
|
||||
},
|
||||
} as never);
|
||||
expect(aliasNormalized).toMatchObject({
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves synthetic auth when ADC is available", async () => {
|
||||
hasAnthropicVertexAvailableAuthMock.mockReturnValue(true);
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
const result = provider.resolveSyntheticAuth?.({
|
||||
provider: "anthropic-vertex",
|
||||
config: undefined,
|
||||
providerConfig: undefined,
|
||||
} as never);
|
||||
|
||||
expect(result).toEqual({
|
||||
apiKey: "gcp-vertex-credentials",
|
||||
source: "gcp-vertex-credentials (ADC)",
|
||||
mode: "api-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when ADC is not available", async () => {
|
||||
hasAnthropicVertexAvailableAuthMock.mockReturnValue(false);
|
||||
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
|
||||
|
||||
const result = provider.resolveSyntheticAuth?.({
|
||||
provider: "anthropic-vertex",
|
||||
config: undefined,
|
||||
providerConfig: undefined,
|
||||
} as never);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
73
extensions/anthropic-vertex/index.ts
Normal file
73
extensions/anthropic-vertex/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Anthropic Vertex provider plugin entry. It registers implicit ADC-backed
|
||||
* catalog discovery, Anthropic replay policy, thinking profiles, and auth markers.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import {
|
||||
NATIVE_ANTHROPIC_REPLAY_HOOKS,
|
||||
resolveClaudeThinkingProfile,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
hasAnthropicVertexAvailableAuth,
|
||||
mergeImplicitAnthropicVertexProvider,
|
||||
resolveAnthropicVertexConfigApiKey,
|
||||
resolveImplicitAnthropicVertexProvider,
|
||||
} from "./api.js";
|
||||
import { normalizeAnthropicVertexResolvedModel } from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "anthropic-vertex";
|
||||
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
|
||||
|
||||
/** Provider entry for Anthropic Claude models served through Google Vertex AI. */
|
||||
export default definePluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
name: "Anthropic Vertex Provider",
|
||||
description: "Bundled Anthropic Vertex provider plugin",
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "Anthropic Vertex",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const implicit = resolveImplicitAnthropicVertexProvider({
|
||||
env: ctx.env,
|
||||
});
|
||||
if (!implicit) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: mergeImplicitAnthropicVertexProvider({
|
||||
existing: ctx.config.models?.providers?.[PROVIDER_ID],
|
||||
implicit,
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
|
||||
...NATIVE_ANTHROPIC_REPLAY_HOOKS,
|
||||
normalizeResolvedModel: ({ modelId, model }) =>
|
||||
normalizeAnthropicVertexResolvedModel(modelId, model),
|
||||
resolveThinkingProfile: ({ modelId, params }) =>
|
||||
resolveClaudeThinkingProfile(modelId, params, { includeNativeMax: true }),
|
||||
resolveSyntheticAuth: () => {
|
||||
if (!hasAnthropicVertexAvailableAuth()) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
|
||||
source: "gcp-vertex-credentials (ADC)",
|
||||
mode: "api-key",
|
||||
};
|
||||
},
|
||||
augmentModelCatalog: ({ config }) =>
|
||||
readConfiguredProviderCatalogEntries({
|
||||
config,
|
||||
providerId: PROVIDER_ID,
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
376
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
Normal file
376
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.11",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.109.1.tgz",
|
||||
"integrity": "sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"standardwebhooks": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/vertex-sdk": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.19.0.tgz",
|
||||
"integrity": "sha512-Ja5NkDAmdCcvCJCvkY/6uJ+9krOiXFN56dzb+8n5apElHrYpUMlOCHCVRuLLsJCVWTsN8w60sgN9RSXZ+LPqNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.50.3 <1",
|
||||
"google-auth-library": "^10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz",
|
||||
"integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.9.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz",
|
||||
"integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^7.1.4",
|
||||
"gcp-metadata": "8.1.2",
|
||||
"google-logging-utils": "1.1.3",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
||||
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"name": "@nolyfill/domexception",
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/@nolyfill/domexception/-/domexception-1.0.28.tgz",
|
||||
"integrity": "sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
extensions/anthropic-vertex/openclaw.plugin.json
Normal file
19
extensions/anthropic-vertex/openclaw.plugin.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "anthropic-vertex",
|
||||
"name": "Anthropic Vertex",
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.",
|
||||
"icon": "https://cdn.simpleicons.org/anthropic",
|
||||
"activation": {
|
||||
"onStartup": false
|
||||
},
|
||||
"enabledByDefault": true,
|
||||
"providers": ["anthropic-vertex"],
|
||||
"providerCatalogEntry": "./provider-discovery.ts",
|
||||
"syntheticAuthRefs": ["anthropic-vertex"],
|
||||
"nonSecretAuthMarkers": ["gcp-vertex-credentials"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
37
extensions/anthropic-vertex/package.json
Normal file
37
extensions/anthropic-vertex/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.11",
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openclaw/openclaw"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "0.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/anthropic-vertex-provider",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.5.12-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.11"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.11",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
}
|
||||
}
|
||||
}
|
||||
139
extensions/anthropic-vertex/provider-catalog.ts
Normal file
139
extensions/anthropic-vertex/provider-catalog.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
|
||||
/**
|
||||
* Static Anthropic Vertex model catalog builder. It derives provider base URLs
|
||||
* from region configuration and publishes Claude model metadata.
|
||||
*/
|
||||
import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { resolveClaudeFable5ModelIdentity } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolveAnthropicVertexRegion } from "./region.js";
|
||||
/** Default Anthropic Vertex model used for implicit provider catalogs. */
|
||||
export const ANTHROPIC_VERTEX_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
|
||||
const ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW = 1_000_000;
|
||||
const ANTHROPIC_VERTEX_FABLE_MAX_TOKENS = 128_000;
|
||||
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
|
||||
|
||||
function buildAnthropicVertexModel(params: {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
input: ModelDefinitionConfig["input"];
|
||||
cost: ModelDefinitionConfig["cost"];
|
||||
maxTokens: number;
|
||||
thinkingLevelMap?: ModelDefinitionConfig["thinkingLevelMap"];
|
||||
}): ModelDefinitionConfig {
|
||||
return {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
reasoning: params.reasoning,
|
||||
input: params.input,
|
||||
cost: params.cost,
|
||||
contextWindow: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: params.maxTokens,
|
||||
...(params.thinkingLevelMap ? { thinkingLevelMap: params.thinkingLevelMap } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAnthropicVertexCatalog(): ModelDefinitionConfig[] {
|
||||
return [
|
||||
buildAnthropicVertexModel({
|
||||
id: "claude-fable-5",
|
||||
name: "Claude Fable 5",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
||||
maxTokens: ANTHROPIC_VERTEX_FABLE_MAX_TOKENS,
|
||||
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
|
||||
}),
|
||||
buildAnthropicVertexModel({
|
||||
id: "claude-opus-4-8",
|
||||
name: "Claude Opus 4.8",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
maxTokens: 128000,
|
||||
thinkingLevelMap: { xhigh: "xhigh", max: "max" },
|
||||
}),
|
||||
buildAnthropicVertexModel({
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
maxTokens: 128000,
|
||||
thinkingLevelMap: { xhigh: null, max: "max" },
|
||||
}),
|
||||
buildAnthropicVertexModel({
|
||||
id: ANTHROPIC_VERTEX_DEFAULT_MODEL_ID,
|
||||
name: "Claude Sonnet 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
maxTokens: 128000,
|
||||
thinkingLevelMap: { xhigh: null, max: "max" },
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/** Restore required Fable metadata after explicit catalog models replace the implicit row. */
|
||||
export function normalizeAnthropicVertexResolvedModel(
|
||||
modelId: string,
|
||||
model: ProviderRuntimeModel,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
if (!resolveClaudeFable5ModelIdentity({ id: modelId, params: model.params })) {
|
||||
return undefined;
|
||||
}
|
||||
const input: ProviderRuntimeModel["input"] = model.input.includes("image")
|
||||
? model.input
|
||||
: [...model.input, "image"];
|
||||
const thinkingLevelMap = {
|
||||
off: "low",
|
||||
minimal: "low",
|
||||
xhigh: "xhigh",
|
||||
max: "max",
|
||||
...model.thinkingLevelMap,
|
||||
};
|
||||
if (
|
||||
model.reasoning &&
|
||||
input === model.input &&
|
||||
model.contextWindow === ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW &&
|
||||
model.contextTokens === ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW &&
|
||||
(model.maxTokens ?? 0) >= ANTHROPIC_VERTEX_FABLE_MAX_TOKENS &&
|
||||
model.thinkingLevelMap?.off === "low" &&
|
||||
model.thinkingLevelMap.minimal === "low" &&
|
||||
model.thinkingLevelMap.xhigh === "xhigh" &&
|
||||
model.thinkingLevelMap.max === "max"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...model,
|
||||
reasoning: true,
|
||||
input,
|
||||
contextWindow: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
|
||||
contextTokens: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: Math.max(model.maxTokens ?? 0, ANTHROPIC_VERTEX_FABLE_MAX_TOKENS),
|
||||
thinkingLevelMap,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the implicit Anthropic Vertex provider config for the current env. */
|
||||
export function buildAnthropicVertexProvider(params?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ModelProviderConfig {
|
||||
const region = resolveAnthropicVertexRegion(params?.env);
|
||||
const baseUrl =
|
||||
normalizeLowercaseStringOrEmpty(region) === "global"
|
||||
? "https://aiplatform.googleapis.com"
|
||||
: `https://${region}-aiplatform.googleapis.com`;
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
api: "anthropic-messages",
|
||||
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
|
||||
models: buildAnthropicVertexCatalog(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Anthropic Vertex tests cover provider discovery.import guard plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("anthropic-vertex provider discovery entry", () => {
|
||||
it("imports without loading the full plugin entry", async () => {
|
||||
const module = await import("./provider-discovery.js");
|
||||
|
||||
expect(module.default.id).toBe("anthropic-vertex");
|
||||
expect(module.default.catalog.order).toBe("simple");
|
||||
});
|
||||
});
|
||||
97
extensions/anthropic-vertex/provider-discovery.ts
Normal file
97
extensions/anthropic-vertex/provider-discovery.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Provider discovery descriptor for Anthropic Vertex. This variant is used by
|
||||
* catalog surfaces that need the provider contract without full plugin entry setup.
|
||||
*/
|
||||
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { buildAnthropicVertexProvider } from "./provider-catalog.js";
|
||||
import { hasAnthropicVertexAvailableAuth, resolveAnthropicVertexConfigApiKey } from "./region.js";
|
||||
|
||||
const PROVIDER_ID = "anthropic-vertex";
|
||||
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
|
||||
|
||||
type AnthropicVertexProviderPlugin = {
|
||||
id: string;
|
||||
label: string;
|
||||
docsPath: string;
|
||||
auth: [];
|
||||
catalog: {
|
||||
order: "simple";
|
||||
run: (ctx: ProviderCatalogContext) => ReturnType<typeof runAnthropicVertexCatalog>;
|
||||
};
|
||||
resolveConfigApiKey: (params: { env: NodeJS.ProcessEnv }) => string | undefined;
|
||||
resolveSyntheticAuth: () =>
|
||||
| {
|
||||
apiKey: string;
|
||||
source: string;
|
||||
mode: "api-key";
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
function mergeImplicitAnthropicVertexProvider(params: {
|
||||
existing?: ModelProviderConfig;
|
||||
implicit: ModelProviderConfig;
|
||||
}) {
|
||||
const { existing, implicit } = params;
|
||||
if (!existing) {
|
||||
return implicit;
|
||||
}
|
||||
return {
|
||||
...implicit,
|
||||
...existing,
|
||||
models:
|
||||
Array.isArray(existing.models) && existing.models.length > 0
|
||||
? existing.models
|
||||
: implicit.models,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveImplicitAnthropicVertexProvider(params?: { env?: NodeJS.ProcessEnv }) {
|
||||
const env = params?.env ?? process.env;
|
||||
if (!hasAnthropicVertexAvailableAuth(env)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildAnthropicVertexProvider({ env });
|
||||
}
|
||||
|
||||
async function runAnthropicVertexCatalog(ctx: ProviderCatalogContext) {
|
||||
const implicit = resolveImplicitAnthropicVertexProvider({
|
||||
env: ctx.env,
|
||||
});
|
||||
if (!implicit) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: mergeImplicitAnthropicVertexProvider({
|
||||
existing: ctx.config.models?.providers?.[PROVIDER_ID],
|
||||
implicit,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Anthropic Vertex provider discovery descriptor. */
|
||||
export const anthropicVertexProviderDiscovery: AnthropicVertexProviderPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
label: "Anthropic Vertex",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: runAnthropicVertexCatalog,
|
||||
},
|
||||
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
|
||||
resolveSyntheticAuth: () => {
|
||||
if (!hasAnthropicVertexAvailableAuth()) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
|
||||
source: "gcp-vertex-credentials (ADC)",
|
||||
mode: "api-key",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default anthropicVertexProviderDiscovery;
|
||||
62
extensions/anthropic-vertex/provider-policy-api.test.ts
Normal file
62
extensions/anthropic-vertex/provider-policy-api.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Anthropic Vertex tests cover provider policy api plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
|
||||
describe("anthropic-vertex provider-policy-api", () => {
|
||||
it("leaves Claude Opus 4.8 thinking off by default with max effort support", () => {
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-opus-4-8",
|
||||
});
|
||||
|
||||
expect(profile?.defaultLevel).toBe("off");
|
||||
expect(profile?.levels.map((level) => level.id)).toContain("max");
|
||||
});
|
||||
|
||||
it("keeps Claude Opus 4.7 thinking off by default", () => {
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-opus-4-7",
|
||||
});
|
||||
|
||||
expect(profile?.defaultLevel).toBe("off");
|
||||
});
|
||||
|
||||
it("exposes native max without xhigh for Claude Sonnet 4.6", () => {
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(profile?.levels.map((level) => level.id)).toContain("max");
|
||||
expect(profile?.levels.map((level) => level.id)).not.toContain("xhigh");
|
||||
});
|
||||
|
||||
it("inherits Claude Fable 5's provider-agnostic thinking contract", () => {
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "claude-fable-5",
|
||||
});
|
||||
|
||||
expect(profile?.defaultLevel).toBe("high");
|
||||
expect(profile?.preserveWhenCatalogReasoningFalse).toBe(true);
|
||||
expect(profile?.levels.map((level) => level.id)).toContain("max");
|
||||
});
|
||||
|
||||
it("resolves deployment aliases from canonical model metadata", () => {
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: "anthropic-vertex",
|
||||
modelId: "production-claude",
|
||||
params: { canonicalModelId: "claude-fable-5" },
|
||||
});
|
||||
|
||||
expect(profile?.defaultLevel).toBe("high");
|
||||
expect(profile?.preserveWhenCatalogReasoningFalse).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores other providers", () => {
|
||||
expect(resolveThinkingProfile({ provider: "anthropic", modelId: "claude-opus-4-8" })).toBe(
|
||||
null,
|
||||
);
|
||||
});
|
||||
});
|
||||
19
extensions/anthropic-vertex/provider-policy-api.ts
Normal file
19
extensions/anthropic-vertex/provider-policy-api.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Provider-policy API for Anthropic Vertex. Core asks for thinking profiles
|
||||
* without importing the provider entry or stream runtime.
|
||||
*/
|
||||
import { resolveClaudeThinkingProfile } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
/** Resolve Anthropic Vertex thinking profile for a provider/model pair. */
|
||||
export function resolveThinkingProfile(params: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
params?: Record<string, unknown>;
|
||||
}) {
|
||||
if (params.provider.trim().toLowerCase() !== "anthropic-vertex") {
|
||||
return null;
|
||||
}
|
||||
return resolveClaudeThinkingProfile(params.modelId, params.params, {
|
||||
includeNativeMax: true,
|
||||
});
|
||||
}
|
||||
83
extensions/anthropic-vertex/region.adc.test.ts
Normal file
83
extensions/anthropic-vertex/region.adc.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Anthropic Vertex tests cover region.adc plugin behavior.
|
||||
import { platform } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({
|
||||
existsSyncMock: vi.fn(),
|
||||
readFileSyncMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname));
|
||||
readFileSyncMock.mockImplementation((pathname, options) =>
|
||||
String(pathname) === "/tmp/vertex-adc.json"
|
||||
? '{"project_id":"vertex-project"}'
|
||||
: actual.readFileSync(pathname, options as never),
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
existsSync: existsSyncMock,
|
||||
readFileSync: readFileSyncMock,
|
||||
default: {
|
||||
...actual,
|
||||
existsSync: existsSyncMock,
|
||||
readFileSync: readFileSyncMock,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { hasAnthropicVertexAvailableAuth, resolveAnthropicVertexProjectId } from "./region.js";
|
||||
|
||||
describe("anthropic-vertex ADC reads", () => {
|
||||
afterEach(() => {
|
||||
existsSyncMock.mockClear();
|
||||
readFileSyncMock.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.doUnmock("node:fs");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("reads explicit ADC credentials without an existsSync preflight", () => {
|
||||
const env = {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/vertex-adc.json",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
existsSyncMock.mockClear();
|
||||
readFileSyncMock.mockClear();
|
||||
|
||||
expect(resolveAnthropicVertexProjectId(env)).toBe("vertex-project");
|
||||
expect(hasAnthropicVertexAvailableAuth(env)).toBe(true);
|
||||
expect(existsSyncMock).not.toHaveBeenCalled();
|
||||
expect(readFileSyncMock).toHaveBeenCalledWith("/tmp/vertex-adc.json", "utf8");
|
||||
});
|
||||
|
||||
it("respects HOME when probing the default ADC path from a copied env snapshot", () => {
|
||||
const homeDir = "/tmp/vertex-home";
|
||||
const defaultAdcPath =
|
||||
platform() === "win32"
|
||||
? path.join(homeDir, "AppData", "Roaming", "gcloud", "application_default_credentials.json")
|
||||
: path.join(homeDir, ".config", "gcloud", "application_default_credentials.json");
|
||||
const env = {
|
||||
HOME: homeDir,
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
readFileSyncMock.mockImplementation((pathname, options) =>
|
||||
String(pathname) === defaultAdcPath
|
||||
? '{"project_id":"vertex-project"}'
|
||||
: String(pathname) === "/tmp/vertex-adc.json"
|
||||
? '{"project_id":"vertex-project"}'
|
||||
: (() => {
|
||||
throw new Error(`unexpected readFileSync(${String(pathname)}, ${String(options)})`);
|
||||
})(),
|
||||
);
|
||||
|
||||
expect(resolveAnthropicVertexProjectId(env)).toBe("vertex-project");
|
||||
expect(hasAnthropicVertexAvailableAuth(env)).toBe(true);
|
||||
expect(existsSyncMock).not.toHaveBeenCalled();
|
||||
expect(readFileSyncMock).toHaveBeenCalledWith(defaultAdcPath, "utf8");
|
||||
});
|
||||
});
|
||||
39
extensions/anthropic-vertex/region.test.ts
Normal file
39
extensions/anthropic-vertex/region.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Anthropic Vertex tests cover region plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveAnthropicVertexRegion, resolveAnthropicVertexRegionFromBaseUrl } from "./api.js";
|
||||
|
||||
describe("anthropic vertex region helpers", () => {
|
||||
it("accepts well-formed regional env values", () => {
|
||||
expect(
|
||||
resolveAnthropicVertexRegion({
|
||||
GOOGLE_CLOUD_LOCATION: "us-east1",
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe("us-east1");
|
||||
});
|
||||
|
||||
it("falls back to the default region for malformed env values", () => {
|
||||
expect(
|
||||
resolveAnthropicVertexRegion({
|
||||
GOOGLE_CLOUD_LOCATION: "us-central1.attacker.example",
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe("global");
|
||||
});
|
||||
|
||||
it("parses regional Vertex endpoints", () => {
|
||||
expect(
|
||||
resolveAnthropicVertexRegionFromBaseUrl("https://europe-west4-aiplatform.googleapis.com"),
|
||||
).toBe("europe-west4");
|
||||
});
|
||||
|
||||
it("treats the global Vertex endpoint as global", () => {
|
||||
expect(resolveAnthropicVertexRegionFromBaseUrl("https://aiplatform.googleapis.com")).toBe(
|
||||
"global",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not infer a Vertex region from custom proxy hosts", () => {
|
||||
expect(
|
||||
resolveAnthropicVertexRegionFromBaseUrl("https://proxy.example.com/google/aiplatform"),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
156
extensions/anthropic-vertex/region.ts
Normal file
156
extensions/anthropic-vertex/region.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Anthropic Vertex region, project, and ADC auth detection helpers. They keep
|
||||
* credential probing local to the provider plugin.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { homedir, platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
const ANTHROPIC_VERTEX_DEFAULT_REGION = "global";
|
||||
const ANTHROPIC_VERTEX_REGION_RE = /^[a-z0-9-]+$/;
|
||||
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
|
||||
|
||||
type AdcProjectFile = {
|
||||
project_id?: unknown;
|
||||
quota_project_id?: unknown;
|
||||
};
|
||||
|
||||
function normalizeOptionalSecretInput(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
/** Resolve the configured Vertex region, defaulting to global. */
|
||||
export function resolveAnthropicVertexRegion(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const region =
|
||||
normalizeOptionalSecretInput(env.GOOGLE_CLOUD_LOCATION) ||
|
||||
normalizeOptionalSecretInput(env.CLOUD_ML_REGION);
|
||||
|
||||
return region && ANTHROPIC_VERTEX_REGION_RE.test(region)
|
||||
? region
|
||||
: ANTHROPIC_VERTEX_DEFAULT_REGION;
|
||||
}
|
||||
|
||||
/** Resolve the Vertex project id from explicit env or ADC files. */
|
||||
export function resolveAnthropicVertexProjectId(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
return (
|
||||
normalizeOptionalSecretInput(env.ANTHROPIC_VERTEX_PROJECT_ID) ||
|
||||
normalizeOptionalSecretInput(env.GOOGLE_CLOUD_PROJECT) ||
|
||||
normalizeOptionalSecretInput(env.GOOGLE_CLOUD_PROJECT_ID) ||
|
||||
resolveAnthropicVertexProjectIdFromAdc(env)
|
||||
);
|
||||
}
|
||||
|
||||
/** Extract a Vertex region from a provider base URL when possible. */
|
||||
export function resolveAnthropicVertexRegionFromBaseUrl(baseUrl?: string): string | undefined {
|
||||
const endpoint = resolveProviderEndpoint(baseUrl);
|
||||
return endpoint.endpointClass === "google-vertex" ? endpoint.googleVertexRegion : undefined;
|
||||
}
|
||||
|
||||
/** Resolve the client region from model base URL first, then env fallback. */
|
||||
export function resolveAnthropicVertexClientRegion(params?: {
|
||||
baseUrl?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
return (
|
||||
resolveAnthropicVertexRegionFromBaseUrl(params?.baseUrl) ||
|
||||
resolveAnthropicVertexRegion(params?.env)
|
||||
);
|
||||
}
|
||||
|
||||
function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const explicitMetadataOptIn = normalizeOptionalSecretInput(env.ANTHROPIC_VERTEX_USE_GCP_METADATA);
|
||||
return (
|
||||
explicitMetadataOptIn === "1" ||
|
||||
normalizeLowercaseStringOrEmpty(explicitMetadataOptIn) === "true"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexHomeDir(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return (
|
||||
normalizeOptionalSecretInput(env.HOME) ||
|
||||
normalizeOptionalSecretInput(env.USERPROFILE) ||
|
||||
homedir()
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return platform() === "win32"
|
||||
? join(
|
||||
normalizeOptionalSecretInput(env.APPDATA) ??
|
||||
join(resolveAnthropicVertexHomeDir(env), "AppData", "Roaming"),
|
||||
"gcloud",
|
||||
"application_default_credentials.json",
|
||||
)
|
||||
: join(
|
||||
resolveAnthropicVertexHomeDir(env),
|
||||
".config",
|
||||
"gcloud",
|
||||
"application_default_credentials.json",
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexAdcCredentialsPathCandidate(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
const explicit = normalizeOptionalSecretInput(env.GOOGLE_APPLICATION_CREDENTIALS);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
return resolveAnthropicVertexDefaultAdcPath(env);
|
||||
}
|
||||
|
||||
function canReadAnthropicVertexAdc(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env);
|
||||
if (!credentialsPath) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
readFileSync(credentialsPath, "utf8");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexProjectIdFromAdc(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env);
|
||||
if (!credentialsPath) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(credentialsPath, "utf8")) as AdcProjectFile;
|
||||
return (
|
||||
normalizeOptionalSecretInput(parsed.project_id) ||
|
||||
normalizeOptionalSecretInput(parsed.quota_project_id)
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Return whether ADC credentials or metadata-server auth are available. */
|
||||
export function hasAnthropicVertexCredentials(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return hasAnthropicVertexMetadataServerAdc(env) || canReadAnthropicVertexAdc(env);
|
||||
}
|
||||
|
||||
/** Return whether Anthropic Vertex has usable auth for implicit registration. */
|
||||
export function hasAnthropicVertexAvailableAuth(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return hasAnthropicVertexCredentials(env);
|
||||
}
|
||||
|
||||
/** Resolve the synthetic config API key marker for Anthropic Vertex auth. */
|
||||
export function resolveAnthropicVertexConfigApiKey(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
return hasAnthropicVertexAvailableAuth(env) ? GCP_VERTEX_CREDENTIALS_MARKER : undefined;
|
||||
}
|
||||
21
extensions/anthropic-vertex/setup-api.ts
Normal file
21
extensions/anthropic-vertex/setup-api.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Lightweight Anthropic Vertex setup entry. It exposes provider auth detection
|
||||
* without importing the stream runtime or Vertex SDK.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { resolveAnthropicVertexConfigApiKey } from "./region.js";
|
||||
|
||||
/** Setup entry for Anthropic Vertex provider auth probing. */
|
||||
export default definePluginEntry({
|
||||
id: "anthropic-vertex",
|
||||
name: "Anthropic Vertex Setup",
|
||||
description: "Lightweight Anthropic Vertex setup hooks",
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "anthropic-vertex",
|
||||
label: "Anthropic Vertex",
|
||||
auth: [],
|
||||
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
|
||||
});
|
||||
},
|
||||
});
|
||||
478
extensions/anthropic-vertex/stream-runtime.test.ts
Normal file
478
extensions/anthropic-vertex/stream-runtime.test.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
// Anthropic Vertex tests cover stream runtime plugin behavior.
|
||||
import { createAssistantMessageEventStream, type Model } from "openclaw/plugin-sdk/llm";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AnthropicVertexStreamDeps } from "./stream-runtime.js";
|
||||
|
||||
function createStreamDeps(): {
|
||||
deps: AnthropicVertexStreamDeps;
|
||||
streamAnthropicMock: ReturnType<typeof vi.fn>;
|
||||
anthropicVertexCtorMock: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const streamAnthropicMock = vi.fn(
|
||||
(..._args: Parameters<AnthropicVertexStreamDeps["streamAnthropic"]>) =>
|
||||
createAssistantMessageEventStream(),
|
||||
);
|
||||
const anthropicVertexCtorMock = vi.fn();
|
||||
const MockAnthropicVertex = function MockAnthropicVertex(options: unknown) {
|
||||
anthropicVertexCtorMock(options);
|
||||
} as unknown as AnthropicVertexStreamDeps["AnthropicVertex"];
|
||||
|
||||
return {
|
||||
deps: {
|
||||
AnthropicVertex: MockAnthropicVertex,
|
||||
streamAnthropic: streamAnthropicMock,
|
||||
},
|
||||
streamAnthropicMock,
|
||||
anthropicVertexCtorMock,
|
||||
};
|
||||
}
|
||||
|
||||
let createAnthropicVertexStreamFn: typeof import("./stream-runtime.js").createAnthropicVertexStreamFn;
|
||||
let createAnthropicVertexStreamFnForModel: typeof import("./stream-runtime.js").createAnthropicVertexStreamFnForModel;
|
||||
|
||||
function makeModel(params: {
|
||||
id: string;
|
||||
maxTokens?: number;
|
||||
params?: Record<string, unknown>;
|
||||
reasoning?: boolean;
|
||||
thinkingLevelMap?: Model<"anthropic-messages">["thinkingLevelMap"];
|
||||
}): Model<"anthropic-messages"> {
|
||||
return {
|
||||
id: params.id,
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic-vertex",
|
||||
reasoning: params.reasoning ?? true,
|
||||
...(params.maxTokens !== undefined ? { maxTokens: params.maxTokens } : {}),
|
||||
...(params.params ? { params: params.params } : {}),
|
||||
...(params.thinkingLevelMap ? { thinkingLevelMap: params.thinkingLevelMap } : {}),
|
||||
} as Model<"anthropic-messages">;
|
||||
}
|
||||
|
||||
type PayloadHook = (payload: unknown, payloadModel: unknown) => Promise<unknown>;
|
||||
|
||||
function streamAnthropicCall(streamAnthropicMock: ReturnType<typeof vi.fn>): unknown[] {
|
||||
const call = streamAnthropicMock.mock.calls[0];
|
||||
if (!call) {
|
||||
throw new Error("Expected streamAnthropic call");
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
function streamTransportOptions(
|
||||
streamAnthropicMock: ReturnType<typeof vi.fn>,
|
||||
): Record<string, unknown> {
|
||||
const options = streamAnthropicCall(streamAnthropicMock)[2];
|
||||
if (!options || typeof options !== "object") {
|
||||
throw new Error("Expected streamAnthropic transport options");
|
||||
}
|
||||
return options as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function captureTransportPayloadHook(
|
||||
onPayload: PayloadHook | undefined,
|
||||
deps: AnthropicVertexStreamDeps,
|
||||
streamAnthropicMock: ReturnType<typeof vi.fn>,
|
||||
) {
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-sonnet-4-6", maxTokens: 64000 });
|
||||
|
||||
void streamFn(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "Hello" }] } as never,
|
||||
{ cacheRetention: "short", ...(onPayload ? { onPayload } : {}) } as never,
|
||||
);
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
|
||||
return { model, onPayload: transportOptions.onPayload as PayloadHook | undefined };
|
||||
}
|
||||
|
||||
// Mirrors the shared anthropic-messages transport output: cache boundary already
|
||||
// split (uncached dynamic suffix) and all four cache_control markers allocated.
|
||||
function buildBudgetedTransportPayload() {
|
||||
return {
|
||||
system: [
|
||||
{ type: "text", text: "Stable prefix", cache_control: { type: "ephemeral" } },
|
||||
{ type: "text", text: "Dynamic suffix" },
|
||||
],
|
||||
tools: [
|
||||
{ name: "exec", input_schema: { type: "object" }, cache_control: { type: "ephemeral" } },
|
||||
],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Hello", cache_control: { type: "ephemeral" } }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "tool_use", id: "t1", name: "exec", input: {} }] },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "t1",
|
||||
content: [],
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function countCacheControlMarkers(payload: unknown): number {
|
||||
let count = 0;
|
||||
const visit = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(visit);
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
if (record.cache_control !== undefined) {
|
||||
count += 1;
|
||||
}
|
||||
visit(record.content);
|
||||
};
|
||||
const record = payload as Record<string, unknown>;
|
||||
visit(record.system);
|
||||
visit(record.tools);
|
||||
visit(record.messages);
|
||||
return count;
|
||||
}
|
||||
|
||||
describe("createAnthropicVertexStreamFn", () => {
|
||||
beforeAll(async () => {
|
||||
({ createAnthropicVertexStreamFn, createAnthropicVertexStreamFnForModel } =
|
||||
await import("./stream-runtime.js"));
|
||||
});
|
||||
|
||||
it("omits projectId when ADC credentials are used without an explicit project", () => {
|
||||
const { deps, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn(undefined, "global", undefined, deps);
|
||||
|
||||
void streamFn(makeModel({ id: "claude-sonnet-4-6", maxTokens: 128000 }), { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledWith({
|
||||
region: "global",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes an explicit baseURL through to the Vertex client", () => {
|
||||
const { deps, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn(
|
||||
"vertex-project",
|
||||
"us-east5",
|
||||
"https://proxy.example.test/vertex/v1",
|
||||
deps,
|
||||
);
|
||||
|
||||
void streamFn(makeModel({ id: "claude-sonnet-4-6", maxTokens: 128000 }), { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledWith({
|
||||
projectId: "vertex-project",
|
||||
region: "us-east5",
|
||||
baseURL: "https://proxy.example.test/vertex/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("restores the canonical API before calling the shared Anthropic transport", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = {
|
||||
...makeModel({ id: "claude-fable-5", maxTokens: 128000 }),
|
||||
api: "openclaw-anthropic-vertex-simple:default",
|
||||
};
|
||||
|
||||
void streamFn(model as never, { messages: [] }, {});
|
||||
|
||||
expect(streamAnthropicCall(streamAnthropicMock)[0]).toMatchObject({
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic-vertex",
|
||||
id: "claude-fable-5",
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults maxTokens to the model limit instead of the old 32000 cap", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-opus-4-6", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, {});
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock).maxTokens).toBe(128000);
|
||||
});
|
||||
|
||||
it("clamps explicit maxTokens to the selected model limit", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-sonnet-4-6", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { maxTokens: 999999 });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock).maxTokens).toBe(128000);
|
||||
});
|
||||
|
||||
it.each(["claude-opus-4-8", "claude-opus-4-7", "claude-fable-5", "claude-mythos-5"])(
|
||||
"omits unsupported temperature for %s",
|
||||
(modelId) => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: modelId, maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { temperature: 0.7 });
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
expect(Object.hasOwn(transportOptions, "temperature")).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves temperature for Vertex models that support custom sampling", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-sonnet-4-6", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { temperature: 0.7 });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock).temperature).toBe(0.7);
|
||||
});
|
||||
|
||||
it("uses Fable 5's always-adaptive Vertex contract", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-fable-5", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { temperature: 0.7 });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock)).toMatchObject({
|
||||
thinkingEnabled: true,
|
||||
effort: "high",
|
||||
maxTokens: 128000,
|
||||
});
|
||||
expect(streamTransportOptions(streamAnthropicMock)).not.toHaveProperty("temperature");
|
||||
});
|
||||
|
||||
it("uses Mythos 5's mandatory adaptive Vertex contract by default", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-mythos-5", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { temperature: 0.7 });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock)).toMatchObject({
|
||||
thinkingEnabled: true,
|
||||
effort: "high",
|
||||
maxTokens: 128000,
|
||||
});
|
||||
expect(streamTransportOptions(streamAnthropicMock)).not.toHaveProperty("temperature");
|
||||
});
|
||||
|
||||
it("uses canonical Claude policy for Vertex deployment aliases", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({
|
||||
id: "production-claude",
|
||||
maxTokens: 128000,
|
||||
params: { canonicalModelId: "claude-opus-4-8" },
|
||||
});
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "xhigh", temperature: 0.7 });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock)).toMatchObject({
|
||||
thinkingEnabled: true,
|
||||
effort: "xhigh",
|
||||
});
|
||||
expect(streamTransportOptions(streamAnthropicMock)).not.toHaveProperty("temperature");
|
||||
});
|
||||
|
||||
it("preserves Fable 5 low effort on Vertex", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-fable-5", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "low" });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock)).toMatchObject({
|
||||
thinkingEnabled: true,
|
||||
effort: "low",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves Fable 5 xhigh effort on Vertex", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-fable-5", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "xhigh" });
|
||||
|
||||
expect(streamTransportOptions(streamAnthropicMock)).toMatchObject({
|
||||
thinkingEnabled: true,
|
||||
effort: "xhigh",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps unsupported xhigh reasoning to high effort for Opus 4.6", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-opus-4-6", maxTokens: 64000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "xhigh" });
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
expect(transportOptions.thinkingEnabled).toBe(true);
|
||||
expect(transportOptions.effort).toBe("high");
|
||||
});
|
||||
|
||||
it("maps xhigh reasoning to xhigh effort for Opus 4.8", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-opus-4-8", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "xhigh" });
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
expect(transportOptions.thinkingEnabled).toBe(true);
|
||||
expect(transportOptions.effort).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("preserves max reasoning for Opus 4.8", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-opus-4-8", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "max" });
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
expect(transportOptions.thinkingEnabled).toBe(true);
|
||||
expect(transportOptions.effort).toBe("max");
|
||||
});
|
||||
|
||||
it("preserves native max reasoning for Sonnet 4.6", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-sonnet-4-6", maxTokens: 128000 });
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "max" });
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
expect(transportOptions.thinkingEnabled).toBe(true);
|
||||
expect(transportOptions.effort).toBe("max");
|
||||
});
|
||||
|
||||
it("honors explicit max opt-outs for Vertex aliases", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({
|
||||
id: "production-claude",
|
||||
params: { canonicalModelId: "claude-sonnet-4-6" },
|
||||
reasoning: false,
|
||||
thinkingLevelMap: { xhigh: null, max: null },
|
||||
});
|
||||
|
||||
void streamFn(model, { messages: [] }, { reasoning: "max", temperature: 0.2 });
|
||||
|
||||
const transportOptions = streamTransportOptions(streamAnthropicMock);
|
||||
expect(transportOptions.effort).toBe("high");
|
||||
expect(transportOptions).not.toHaveProperty("temperature");
|
||||
});
|
||||
|
||||
it("keeps already-budgeted cache_control markers intact when forwarding payload hooks", async () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const onPayload = vi.fn(async (payload: unknown) => payload);
|
||||
const { model, onPayload: transportPayloadHook } = captureTransportPayloadHook(
|
||||
onPayload,
|
||||
deps,
|
||||
streamAnthropicMock,
|
||||
);
|
||||
const payload = buildBudgetedTransportPayload();
|
||||
|
||||
const nextPayload = await transportPayloadHook?.(payload, model);
|
||||
|
||||
expect(onPayload).toHaveBeenCalledWith(payload, model);
|
||||
expect(countCacheControlMarkers(nextPayload)).toBe(4);
|
||||
expect((nextPayload as ReturnType<typeof buildBudgetedTransportPayload>).system[1]).toEqual({
|
||||
type: "text",
|
||||
text: "Dynamic suffix",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits the transport payload hook when the caller provides none", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const { onPayload: transportPayloadHook } = captureTransportPayloadHook(
|
||||
undefined,
|
||||
deps,
|
||||
streamAnthropicMock,
|
||||
);
|
||||
|
||||
expect(transportPayloadHook).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits maxTokens when neither the model nor request provide a finite limit", () => {
|
||||
const { deps, streamAnthropicMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFn("vertex-project", "us-east5", undefined, deps);
|
||||
const model = makeModel({ id: "claude-sonnet-4-6" });
|
||||
|
||||
void streamFn(model, { messages: [] }, { maxTokens: Number.NaN });
|
||||
|
||||
expect(streamAnthropicMock).toHaveBeenCalledTimes(1);
|
||||
const [calledModel, payload, transportOptions] = streamAnthropicCall(streamAnthropicMock);
|
||||
expect(calledModel).toBe(model);
|
||||
expect(payload).toEqual({ messages: [] });
|
||||
expect(transportOptions).toBeTypeOf("object");
|
||||
expect(Object.hasOwn(transportOptions as object, "maxTokens")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAnthropicVertexStreamFnForModel", () => {
|
||||
it("derives project and region from the model and env", () => {
|
||||
const { deps, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFnForModel(
|
||||
{ baseUrl: "https://europe-west4-aiplatform.googleapis.com" },
|
||||
{ GOOGLE_CLOUD_PROJECT_ID: "vertex-project" } as NodeJS.ProcessEnv,
|
||||
deps,
|
||||
);
|
||||
|
||||
void streamFn(makeModel({ id: "claude-sonnet-4-6", maxTokens: 64000 }), { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledWith({
|
||||
projectId: "vertex-project",
|
||||
region: "europe-west4",
|
||||
baseURL: "https://europe-west4-aiplatform.googleapis.com/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves explicit custom provider base URLs", () => {
|
||||
const { deps, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFnForModel(
|
||||
{ baseUrl: "https://proxy.example.test/custom-root/v1" },
|
||||
{ GOOGLE_CLOUD_PROJECT_ID: "vertex-project" } as NodeJS.ProcessEnv,
|
||||
deps,
|
||||
);
|
||||
|
||||
void streamFn(makeModel({ id: "claude-sonnet-4-6", maxTokens: 64000 }), { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledWith({
|
||||
projectId: "vertex-project",
|
||||
region: "global",
|
||||
baseURL: "https://proxy.example.test/custom-root/v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("adds /v1 for path-prefixed custom provider base URLs", () => {
|
||||
const { deps, anthropicVertexCtorMock } = createStreamDeps();
|
||||
const streamFn = createAnthropicVertexStreamFnForModel(
|
||||
{ baseUrl: "https://proxy.example.test/custom-root" },
|
||||
{ GOOGLE_CLOUD_PROJECT_ID: "vertex-project" } as NodeJS.ProcessEnv,
|
||||
deps,
|
||||
);
|
||||
|
||||
void streamFn(makeModel({ id: "claude-sonnet-4-6", maxTokens: 64000 }), { messages: [] }, {});
|
||||
|
||||
expect(anthropicVertexCtorMock).toHaveBeenCalledWith({
|
||||
projectId: "vertex-project",
|
||||
region: "global",
|
||||
baseURL: "https://proxy.example.test/custom-root/v1",
|
||||
});
|
||||
});
|
||||
});
|
||||
245
extensions/anthropic-vertex/stream-runtime.ts
Normal file
245
extensions/anthropic-vertex/stream-runtime.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Anthropic Vertex stream runtime. It constructs Vertex SDK clients and adapts
|
||||
* OpenClaw stream options for the shared Anthropic Messages transport.
|
||||
*/
|
||||
import { AnthropicVertex as AnthropicVertexSdk } from "@anthropic-ai/vertex-sdk";
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import {
|
||||
clampThinkingLevel,
|
||||
stream as streamDefault,
|
||||
type Model,
|
||||
type ModelThinkingLevel,
|
||||
type ProviderStreamOptions,
|
||||
} from "openclaw/plugin-sdk/llm";
|
||||
import {
|
||||
resolveClaudeFable5ModelIdentity,
|
||||
resolveClaudeModelIdentity,
|
||||
supportsClaudeAdaptiveThinking,
|
||||
supportsClaudeNativeMaxEffort,
|
||||
supportsClaudeNativeXhighEffort,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexProjectId } from "./region.js";
|
||||
|
||||
type AnthropicVertexTransportOptions = ProviderStreamOptions & {
|
||||
client?: unknown;
|
||||
thinkingEnabled?: boolean;
|
||||
thinkingBudgetTokens?: number;
|
||||
effort?: "low" | "medium" | "high" | "xhigh" | "max";
|
||||
};
|
||||
|
||||
type AnthropicVertexEffort = NonNullable<AnthropicVertexTransportOptions["effort"]>;
|
||||
type AnthropicVertexAdaptiveEffort = AnthropicVertexEffort | "xhigh";
|
||||
type AnthropicVertexClientOptions = {
|
||||
baseURL?: string;
|
||||
projectId?: string;
|
||||
region: string;
|
||||
};
|
||||
|
||||
/** Injectable dependencies for Anthropic Vertex stream tests. */
|
||||
export type AnthropicVertexStreamDeps = {
|
||||
AnthropicVertex: new (options: AnthropicVertexClientOptions) => unknown;
|
||||
streamAnthropic: typeof streamDefault;
|
||||
};
|
||||
|
||||
const defaultAnthropicVertexStreamDeps: AnthropicVertexStreamDeps = {
|
||||
AnthropicVertex: AnthropicVertexSdk as AnthropicVertexStreamDeps["AnthropicVertex"],
|
||||
streamAnthropic: streamDefault,
|
||||
};
|
||||
|
||||
function isClaudeOpus47OrNewerModel(modelId: string): boolean {
|
||||
return supportsClaudeNativeXhighEffort({ id: modelId });
|
||||
}
|
||||
|
||||
function isClaudeFable5Model(modelId: string): boolean {
|
||||
return resolveClaudeFable5ModelIdentity({ id: modelId }) !== undefined;
|
||||
}
|
||||
|
||||
function isClaudeMythos5Model(modelId: string): boolean {
|
||||
return /(?:^|-)claude-mythos-5(?=$|[^a-z0-9])/.test(resolveClaudeModelIdentity({ id: modelId }));
|
||||
}
|
||||
|
||||
function supportsAdaptiveThinking(modelId: string): boolean {
|
||||
return supportsClaudeAdaptiveThinking({ id: modelId }) || isClaudeMythos5Model(modelId);
|
||||
}
|
||||
|
||||
function mapAnthropicAdaptiveEffort(
|
||||
reasoning: ModelThinkingLevel,
|
||||
model: Model<"anthropic-messages">,
|
||||
modelId: string,
|
||||
): AnthropicVertexAdaptiveEffort {
|
||||
const clampModel =
|
||||
typeof model.params?.canonicalModelId === "string" ? { ...model, reasoning: true } : model;
|
||||
const resolvedReasoning = clampThinkingLevel(clampModel, reasoning);
|
||||
const mapped = model.thinkingLevelMap?.[resolvedReasoning];
|
||||
if (typeof mapped === "string") {
|
||||
return mapped as AnthropicVertexAdaptiveEffort;
|
||||
}
|
||||
const effortMap: Record<string, AnthropicVertexAdaptiveEffort> = {
|
||||
off: "low",
|
||||
minimal: "low",
|
||||
low: "low",
|
||||
medium: "medium",
|
||||
high: "high",
|
||||
xhigh: isClaudeFable5Model(modelId)
|
||||
? "xhigh"
|
||||
: isClaudeOpus47OrNewerModel(modelId) || isClaudeMythos5Model(modelId)
|
||||
? "xhigh"
|
||||
: "high",
|
||||
max:
|
||||
supportsClaudeNativeMaxEffort({ id: modelId }) || isClaudeMythos5Model(modelId)
|
||||
? "max"
|
||||
: "high",
|
||||
};
|
||||
return effortMap[resolvedReasoning] ?? "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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a StreamFn that routes through OpenClaw's generic model stream with an
|
||||
* injected `AnthropicVertex` client. All streaming, message conversion, and
|
||||
* event handling is handled by the shared model runtime - we only supply the GCP-authenticated
|
||||
* client and provider transport options.
|
||||
*/
|
||||
export function createAnthropicVertexStreamFn(
|
||||
projectId: string | undefined,
|
||||
region: string,
|
||||
baseURL?: string,
|
||||
deps: AnthropicVertexStreamDeps = defaultAnthropicVertexStreamDeps,
|
||||
): StreamFn {
|
||||
const client = new deps.AnthropicVertex({
|
||||
region,
|
||||
...(baseURL ? { baseURL } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
});
|
||||
|
||||
return (model, context, options) => {
|
||||
// Simple completions use a synthetic registry API to select this plugin.
|
||||
// The shared Anthropic transport must receive its canonical API or it recurses.
|
||||
const transportModel = (
|
||||
model.api === "anthropic-messages" ? model : { ...model, api: "anthropic-messages" as const }
|
||||
) as Model<"anthropic-messages"> & {
|
||||
baseUrl?: string;
|
||||
provider: string;
|
||||
};
|
||||
const maxTokens = resolveAnthropicVertexMaxTokens({
|
||||
modelMaxTokens: transportModel.maxTokens,
|
||||
requestedMaxTokens: options?.maxTokens,
|
||||
});
|
||||
const contractModelId = resolveClaudeModelIdentity(model);
|
||||
const fable5 = isClaudeFable5Model(contractModelId);
|
||||
const mandatoryAdaptiveThinking = fable5 || isClaudeMythos5Model(contractModelId);
|
||||
const reasoning =
|
||||
(options?.reasoning as ModelThinkingLevel | undefined) ??
|
||||
(mandatoryAdaptiveThinking ? "high" : undefined);
|
||||
const adaptiveThinking =
|
||||
mandatoryAdaptiveThinking || Boolean(reasoning && supportsAdaptiveThinking(contractModelId));
|
||||
const temperature =
|
||||
adaptiveThinking ||
|
||||
isClaudeOpus47OrNewerModel(contractModelId) ||
|
||||
isClaudeMythos5Model(contractModelId)
|
||||
? undefined
|
||||
: options?.temperature;
|
||||
const opts: AnthropicVertexTransportOptions = {
|
||||
client,
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(maxTokens !== undefined ? { maxTokens } : {}),
|
||||
signal: options?.signal,
|
||||
cacheRetention: options?.cacheRetention,
|
||||
sessionId: options?.sessionId,
|
||||
headers: options?.headers,
|
||||
// The shared anthropic-messages transport already splits the system prompt
|
||||
// cache boundary and budgets all cache_control markers; re-applying the
|
||||
// payload policy here marked the uncached suffix and breached the 4-marker cap.
|
||||
onPayload: options?.onPayload,
|
||||
maxRetryDelayMs: options?.maxRetryDelayMs,
|
||||
metadata: options?.metadata,
|
||||
};
|
||||
|
||||
if (reasoning) {
|
||||
if (supportsAdaptiveThinking(contractModelId)) {
|
||||
opts.thinkingEnabled = true;
|
||||
opts.effort = mapAnthropicAdaptiveEffort(
|
||||
reasoning,
|
||||
transportModel,
|
||||
contractModelId,
|
||||
) as AnthropicVertexEffort;
|
||||
} else {
|
||||
opts.thinkingEnabled = true;
|
||||
const budgets = options?.thinkingBudgets;
|
||||
opts.thinkingBudgetTokens =
|
||||
(budgets && reasoning in budgets
|
||||
? budgets[reasoning as keyof typeof budgets]
|
||||
: undefined) ?? 10000;
|
||||
}
|
||||
} else if (fable5) {
|
||||
opts.thinkingEnabled = true;
|
||||
opts.effort = "high";
|
||||
} else {
|
||||
opts.thinkingEnabled = false;
|
||||
}
|
||||
|
||||
return deps.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;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create an Anthropic Vertex stream function from model metadata and env. */
|
||||
export function createAnthropicVertexStreamFnForModel(
|
||||
model: { baseUrl?: string },
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
deps?: AnthropicVertexStreamDeps,
|
||||
): StreamFn {
|
||||
return createAnthropicVertexStreamFn(
|
||||
resolveAnthropicVertexProjectId(env),
|
||||
resolveAnthropicVertexClientRegion({
|
||||
baseUrl: model.baseUrl,
|
||||
env,
|
||||
}),
|
||||
resolveAnthropicVertexSdkBaseUrl(model.baseUrl),
|
||||
deps,
|
||||
);
|
||||
}
|
||||
16
extensions/anthropic-vertex/tsconfig.json
Normal file
16
extensions/anthropic-vertex/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../tsconfig.package-boundary.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./*.ts", "./src/**/*.ts"],
|
||||
"exclude": [
|
||||
"./**/*.test.ts",
|
||||
"./dist/**",
|
||||
"./node_modules/**",
|
||||
"./src/test-support/**",
|
||||
"./src/**/*test-helpers.ts",
|
||||
"./src/**/*test-harness.ts",
|
||||
"./src/**/*test-support.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user