fix(deps): remove deprecated uuid install path by replacing vertex-sdk with local client (#1771)

* fix(deps): remove deprecated uuid install path

* fix(api): address PR #1232 review — type the local Vertex client surface

Resolves the blocker raised by @Vasanthdev2004, @gnanam1990, and @jatmn: the
in-repo AnthropicVertex replacement compiled under bun (no type-check) but added
4 `tsc --noEmit` errors that the upstream typed SDK did not.

- Declare `messages`/`beta` as typed class fields (BaseAnthropic doesn't, but
  the upstream @anthropic-ai/vertex-sdk client did), so typed consumers —
  client.ts `new AnthropicVertex(...)` and the SDK calling `.messages` — keep
  the resource surface. (vertexClient.ts:145/146, test:53)
- Widen the header-merge helpers to accept the base client's request header type
  (HeadersLike), and handle the NullableHeaders shape it actually passes so the
  merge stays correct, not just type-clean. (vertexClient.ts:182)

Also drops the now-stale `@anthropic-ai/vertex-sdk` entries left behind by the
dependency removal:
- scripts/externals.ts INTENTIONALLY_BUNDLED (P3)
- knip.json ignoreDependencies

Testing: `tsc --noEmit` clean; vertex/client/gemini tests 51 pass; smoke green
(INTENTIONALLY_BUNDLED back in sync, 57 entries); knip clean.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): address CodeRabbit review on PR #1232 — auth precedence + coverage

- Security (vertexClient.ts): merge resolved Google auth headers LAST so a
  caller-supplied Authorization / x-goog-user-project can't override the Vertex
  credential and send the wrong token upstream. Other request headers still
  pass through unchanged.
- Tests (vertexClient.test.ts): add focused regression coverage for the
  previously-unguarded routing/auth branches —
    * streaming → :streamRawPredict path (+ model stripped, stream preserved)
    * count_tokens → count-tokens:rawPredict path rewrite
    * auth-header precedence: caller Authorization does NOT override the Vertex
      token (guards the fix above + exercises the NullableHeaders merge branch).

Testing: tsc clean; vertexClient tests 5 pass; full src/services/api 840 pass;
smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): validate and encode Vertex model before building the URL

CodeRabbit follow-up on PR #1232: `model` was interpolated straight into the
Vertex endpoint path, so a missing/non-string model would silently route to
`.../models/undefined:rawPredict` instead of failing fast. Now throw a clear
error on a missing/empty model and encodeURIComponent the value before building
the path. Adds a focused test for the missing-model case.

Testing: tsc clean; vertexClient tests 6 pass; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): address remaining review items from PR #1232

- [P2] Fix count_tokens method guard: apply method==='post' to both paths
- [P3] Remove unused accessToken option from AnthropicVertex
- [P3] Narrow batches type on messages/beta resources with Omit

* test: add count_tokens?beta=true routing regression test

---------

Co-authored-by: Kevin Codex <kevin@gitlawb.com>
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
This commit is contained in:
Gravirei
2026-06-24 13:03:29 +08:00
committed by GitHub
co-authored by Kevin Codex OpenClaude Gravirei
parent 3eb57c6d13
commit 9c0d5c61e2
7 changed files with 555 additions and 29 deletions
+16 -20
View File
@@ -10,7 +10,6 @@
"@anthropic-ai/foundry-sdk": "0.2.3",
"@anthropic-ai/sandbox-runtime": "0.0.55",
"@anthropic-ai/sdk": "0.94.0",
"@anthropic-ai/vertex-sdk": "0.16.0",
"@commander-js/extra-typings": "12.1.0",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
@@ -37,7 +36,7 @@
"figures": "6.1.0",
"fuse.js": "7.1.0",
"get-east-asian-width": "1.5.0",
"google-auth-library": "9.15.1",
"google-auth-library": "10.6.2",
"https-proxy-agent": "7.0.6",
"ignore": "7.0.5",
"indent-string": "5.0.0",
@@ -79,6 +78,7 @@
},
},
"overrides": {
"google-auth-library": "10.6.2",
"ip-address": "10.2.0",
"lodash-es": "4.18.1",
},
@@ -93,8 +93,6 @@
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.94.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-OVlCttk5MyeTGtrWX5+F3MJOfEMDuEjK8+rm9aQMDfRPWndVMbhk37QG8WLnVbcc7huyUGngVMjT7iMN2llySA=="],
"@anthropic-ai/vertex-sdk": ["@anthropic-ai/vertex-sdk@0.16.0", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "google-auth-library": "^9.4.2" } }, "sha512-ntxemtRkwPsjVzGQJsmBPRW38tfas6VuVlD1v6pHffDJKLPtCdaiN9KUQeraJ/F34tjxEWlsaCnl3t/orJm1Xw=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
@@ -525,6 +523,8 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
@@ -593,6 +593,8 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
@@ -607,6 +609,8 @@
"formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
@@ -615,9 +619,9 @@
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
"gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
"gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="],
"gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
@@ -631,16 +635,14 @@
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
"google-auth-library": ["google-auth-library@10.6.2", "", { "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" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
@@ -737,7 +739,9 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
@@ -887,8 +891,6 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
@@ -915,8 +917,6 @@
"usehooks-ts": ["usehooks-ts@3.1.1", "", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
@@ -927,9 +927,7 @@
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1219,8 +1217,6 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"knip/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"knip/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+1 -2
View File
@@ -30,7 +30,6 @@
],
"ignoreDependencies": [
"@types/bun",
"@anthropic-ai/foundry-sdk",
"@anthropic-ai/vertex-sdk"
"@anthropic-ai/foundry-sdk"
]
}
+2 -2
View File
@@ -79,7 +79,6 @@
"@anthropic-ai/foundry-sdk": "0.2.3",
"@anthropic-ai/sandbox-runtime": "0.0.55",
"@anthropic-ai/sdk": "0.94.0",
"@anthropic-ai/vertex-sdk": "0.16.0",
"@commander-js/extra-typings": "12.1.0",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
@@ -106,7 +105,7 @@
"figures": "6.1.0",
"fuse.js": "7.1.0",
"get-east-asian-width": "1.5.0",
"google-auth-library": "9.15.1",
"google-auth-library": "10.6.2",
"https-proxy-agent": "7.0.6",
"ignore": "7.0.5",
"indent-string": "5.0.0",
@@ -168,6 +167,7 @@
},
"overrides": {
"ip-address": "10.2.0",
"google-auth-library": "10.6.2",
"lodash-es": "4.18.1"
}
}
-1
View File
@@ -59,7 +59,6 @@ export const INTENTIONALLY_BUNDLED: string[] = [
'@anthropic-ai/bedrock-sdk',
'@anthropic-ai/foundry-sdk',
'@anthropic-ai/sandbox-runtime',
'@anthropic-ai/vertex-sdk',
// CLI / TUI utilities
'@alcalzone/ansi-tokenize',
'@commander-js/extra-typings',
+2 -4
View File
@@ -48,6 +48,7 @@ import {
shouldUseFirstPartyAnthropicAuth,
type ProviderOverride,
} from './authRouting.js'
import { AnthropicVertex } from './vertexClient.js'
const importRuntimeModule = new Function(
'specifier',
@@ -564,10 +565,7 @@ export async function getAnthropicClient({
await refreshGcpCredentialsIfNeeded()
}
const [{ AnthropicVertex }, { GoogleAuth }] = await Promise.all([
importRuntimeModule('@anthropic-ai/vertex-sdk'),
importRuntimeModule('google-auth-library'),
])
const { GoogleAuth } = await importRuntimeModule('google-auth-library')
// TODO: Cache either GoogleAuth instance or AuthClient to improve performance
// Currently we create a new GoogleAuth instance for every getAnthropicClient() call
// This could cause repeated authentication flows and metadata server checks
+248
View File
@@ -0,0 +1,248 @@
import { expect, test } from 'bun:test'
import { AnthropicVertex } from './vertexClient.js'
test('routes message requests through Vertex rawPredict with auth headers', async () => {
let capturedUrl: string | undefined
let capturedHeaders: Headers | undefined
let capturedBody: Record<string, unknown> | undefined
const client = new AnthropicVertex({
region: 'us-east5',
authClient: {
getRequestHeaders: () =>
new Headers({
Authorization: 'Bearer vertex-token',
'x-goog-user-project': 'vertex-project',
}),
},
maxRetries: 0,
fetch: (async (input, init) => {
capturedUrl =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
capturedHeaders = new Headers(init?.headers)
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>
return new Response(
JSON.stringify({
id: 'msg_vertex',
type: 'message',
role: 'assistant',
model: 'claude-sonnet-4-6',
content: [{ type: 'text', text: 'ok' }],
stop_reason: 'end_turn',
stop_sequence: null,
usage: {
input_tokens: 1,
output_tokens: 1,
},
}),
{
headers: {
'Content-Type': 'application/json',
},
},
)
}) as typeof fetch,
})
const response = await client.messages.create({
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
})
expect(capturedUrl).toBe(
'https://us-east5-aiplatform.googleapis.com/v1/projects/vertex-project/locations/us-east5/publishers/anthropic/models/claude-sonnet-4-6:rawPredict',
)
expect(capturedHeaders?.get('authorization')).toBe('Bearer vertex-token')
expect(capturedBody?.anthropic_version).toBe('vertex-2023-10-16')
expect(capturedBody).not.toHaveProperty('model')
expect(response).toMatchObject({
id: 'msg_vertex',
role: 'assistant',
model: 'claude-sonnet-4-6',
})
})
test('requires an explicit Vertex auth provider', () => {
expect(() => new AnthropicVertex({ region: 'us-east5' })).toThrow(
'A `googleAuth` or `authClient` option is required.',
)
})
// ─── Regression coverage for the remaining routing/auth branches ──────
type Captured = {
url?: string
headers?: Headers
body?: Record<string, unknown>
}
const MESSAGE_PAYLOAD = {
id: 'msg_vertex',
type: 'message',
role: 'assistant',
model: 'claude-sonnet-4-6',
content: [{ type: 'text', text: 'ok' }],
stop_reason: 'end_turn',
stop_sequence: null,
usage: { input_tokens: 1, output_tokens: 1 },
}
function captureRequest(capture: Captured, init: RequestInit | undefined, input: unknown): void {
capture.url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: (input as Request).url
capture.headers = new Headers(init?.headers)
if (init?.body) {
capture.body = JSON.parse(String(init.body)) as Record<string, unknown>
}
}
function jsonFetch(capture: Captured, payload: unknown): typeof fetch {
return (async (input, init) => {
captureRequest(capture, init, input)
return new Response(JSON.stringify(payload), {
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
}
function sseFetch(capture: Captured): typeof fetch {
const events = [
{ event: 'message_start', data: { type: 'message_start', message: MESSAGE_PAYLOAD } },
{ event: 'content_block_start', data: { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } } },
{ event: 'content_block_delta', data: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'ok' } } },
{ event: 'content_block_stop', data: { type: 'content_block_stop', index: 0 } },
{ event: 'message_delta', data: { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 1 } } },
{ event: 'message_stop', data: { type: 'message_stop' } },
]
const sse = events.map(e => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`).join('')
return (async (input, init) => {
captureRequest(capture, init, input)
return new Response(sse, { headers: { 'Content-Type': 'text/event-stream' } })
}) as typeof fetch
}
function makeClient(
fetchImpl: typeof fetch,
extraAuthHeaders?: Record<string, string>,
): AnthropicVertex {
return new AnthropicVertex({
region: 'us-east5',
authClient: {
getRequestHeaders: () =>
new Headers({
Authorization: 'Bearer vertex-token',
'x-goog-user-project': 'vertex-project',
...extraAuthHeaders,
}),
},
maxRetries: 0,
fetch: fetchImpl,
})
}
test('routes streaming requests through Vertex streamRawPredict', async () => {
const capture: Captured = {}
const client = makeClient(sseFetch(capture))
const stream = await client.messages.create({
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
stream: true,
})
const events: unknown[] = []
for await (const event of stream) {
events.push(event)
}
expect(capture.url).toBe(
'https://us-east5-aiplatform.googleapis.com/v1/projects/vertex-project/locations/us-east5/publishers/anthropic/models/claude-sonnet-4-6:streamRawPredict',
)
expect(capture.body).not.toHaveProperty('model')
expect(capture.body?.stream).toBe(true)
expect(events.length).toBeGreaterThan(0)
})
test('rewrites count_tokens to the Vertex count-tokens:rawPredict endpoint', async () => {
const capture: Captured = {}
const client = makeClient(jsonFetch(capture, { input_tokens: 42 }))
const result = await client.messages.countTokens({
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: 'hello' }],
})
expect(capture.url).toBe(
'https://us-east5-aiplatform.googleapis.com/v1/projects/vertex-project/locations/us-east5/publishers/anthropic/models/count-tokens:rawPredict',
)
expect(capture.body?.anthropic_version).toBe('vertex-2023-10-16')
expect(result).toMatchObject({ input_tokens: 42 })
})
test('resolved Vertex auth headers win over caller-supplied Authorization', async () => {
const capture: Captured = {}
const client = makeClient(jsonFetch(capture, MESSAGE_PAYLOAD))
await client.messages.create(
{
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 64,
},
{ headers: { Authorization: 'Bearer caller-should-not-win' } },
)
// The Google auth credential must not be overridable by caller headers.
expect(capture.headers?.get('authorization')).toBe('Bearer vertex-token')
})
test('rewrites count_tokens?beta=true through the same Vertex endpoint', async () => {
const client = new AnthropicVertex({
region: 'us-east5',
projectId: 'vertex-project',
authClient: { getRequestHeaders: () => new Headers({ Authorization: 'Bearer t' }) },
maxRetries: 0,
})
const options = {
method: 'post',
path: '/v1/messages/count_tokens?beta=true',
body: { messages: [{ role: 'user', content: 'hi' }] },
} as Parameters<typeof client.buildRequest>[0]
await client.buildRequest(options)
expect(options.path).toBe(
'/projects/vertex-project/locations/us-east5/publishers/anthropic/models/count-tokens:rawPredict',
)
expect((options.body as Record<string, unknown>)?.anthropic_version).toBe('vertex-2023-10-16')
})
test('rejects a message request with a missing/invalid model', async () => {
const client = new AnthropicVertex({
region: 'us-east5',
projectId: 'vertex-project',
authClient: { getRequestHeaders: () => new Headers() },
maxRetries: 0,
})
await expect(
client.buildRequest({
method: 'post',
path: '/v1/messages',
body: { messages: [], max_tokens: 1 },
} as Parameters<typeof client.buildRequest>[0]),
).rejects.toThrow('Expected `model` to be a non-empty string')
})
+286
View File
@@ -0,0 +1,286 @@
import { BaseAnthropic, type ClientOptions } from '@anthropic-ai/sdk/client'
import * as Resources from '@anthropic-ai/sdk/resources/index'
const DEFAULT_VERSION = 'vertex-2023-10-16'
const MODEL_ENDPOINTS = new Set(['/v1/messages', '/v1/messages?beta=true'])
type VertexAuthHeaders = HeadersInit | Record<string, string | undefined>
// The base client passes its already-parsed request headers into prepareOptions
// (HeadersLike, which includes the internal NullableHeaders wrapper). Accept
// that shape in the merge helpers in addition to the google-auth header shapes
// so the types line up with what the SDK actually hands us.
type RequestHeaders = Parameters<BaseAnthropic['prepareOptions']>[0]['headers']
type VertexAuthClient = {
projectId?: string | null
getRequestHeaders: () => VertexAuthHeaders | Promise<VertexAuthHeaders>
}
type VertexGoogleAuth = {
getClient: () => VertexAuthClient | Promise<VertexAuthClient>
}
type AnthropicVertexOptions = Omit<ClientOptions, 'baseURL'> & {
authClient?: VertexAuthClient
baseURL?: string | null
googleAuth?: VertexGoogleAuth
projectId?: string | null
region?: string | null
}
function readEnv(name: string): string | undefined {
return typeof process === 'undefined' ? undefined : process.env[name]
}
function isObj(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function setHeader(
target: Headers,
key: string,
value: string | undefined | null,
): void {
if (value !== undefined && value !== null) {
target.set(key, value)
}
}
function appendHeaders(
target: Headers,
source: VertexAuthHeaders | RequestHeaders,
): void {
if (!source) {
return
}
if (source instanceof Headers) {
source.forEach((value, key) => target.set(key, value))
return
}
// NullableHeaders (the base client's parsed request headers):
// { values: Headers, nulls: Set<string> } — copy values, honor explicit unsets.
if (
typeof source === 'object' &&
!Array.isArray(source) &&
'values' in source &&
source.values instanceof Headers
) {
source.values.forEach((value, key) => target.set(key, value))
const nulls = (source as { nulls?: unknown }).nulls
if (nulls instanceof Set) {
for (const key of nulls as Set<string>) {
target.delete(key)
}
}
return
}
if (Array.isArray(source)) {
for (const [key, value] of source) {
setHeader(target, key as string, value as string | undefined | null)
}
return
}
for (const [key, value] of Object.entries(source)) {
if (Array.isArray(value)) {
for (const v of value) {
setHeader(target, key, v as string | undefined | null)
}
} else {
setHeader(target, key, value as string | undefined | null)
}
}
}
function mergeHeaders(
...sources: (VertexAuthHeaders | RequestHeaders)[]
): Headers {
const headers = new Headers()
for (const source of sources) {
appendHeaders(headers, source)
}
return headers
}
function getHeaderValue(
source: VertexAuthHeaders,
headerName: string,
): string | undefined {
if (source instanceof Headers) {
return source.get(headerName) ?? undefined
}
const normalizedName = headerName.toLowerCase()
if (Array.isArray(source)) {
for (const [key, value] of source) {
if (key.toLowerCase() === normalizedName) {
return value
}
}
return undefined
}
for (const [key, value] of Object.entries(source)) {
if (key.toLowerCase() === normalizedName) {
return value
}
}
return undefined
}
export class AnthropicVertex extends BaseAnthropic {
// Re-declare the resource surface that the upstream @anthropic-ai/vertex-sdk
// client exposed. BaseAnthropic does not declare these, so typed consumers
// (client.ts, the SDK calling `.messages`) would otherwise lose the types.
messages: Omit<Resources.Messages, 'batches'>
beta: Omit<Resources.Beta, 'messages'> & { messages: Omit<Resources.Beta.Messages, 'batches'> }
region: string
projectId: string | null
private readonly authClientPromise: Promise<VertexAuthClient>
constructor({
authClient,
baseURL = readEnv('ANTHROPIC_VERTEX_BASE_URL'),
googleAuth,
projectId = readEnv('ANTHROPIC_VERTEX_PROJECT_ID') ?? null,
region = readEnv('CLOUD_ML_REGION') ?? null,
...opts
}: AnthropicVertexOptions = {}) {
if (!region) {
throw new Error(
'No region was given. The client should be instantiated with the `region` option or the `CLOUD_ML_REGION` environment variable should be set.',
)
}
let resolvedBaseURL = baseURL
if (!resolvedBaseURL) {
switch (region) {
case 'global':
resolvedBaseURL = 'https://aiplatform.googleapis.com/v1'
break
case 'us':
resolvedBaseURL = 'https://aiplatform.us.rep.googleapis.com/v1'
break
case 'eu':
resolvedBaseURL = 'https://aiplatform.eu.rep.googleapis.com/v1'
break
default:
resolvedBaseURL = `https://${region}-aiplatform.googleapis.com/v1`
}
}
super({
baseURL: resolvedBaseURL,
...opts,
})
this.messages = makeMessagesResource(this)
this.beta = makeBetaResource(this)
this.region = region
this.projectId = projectId
if (authClient && googleAuth) {
throw new Error(
'You cannot provide both `authClient` and `googleAuth`. Please provide only one of them.',
)
}
if (authClient) {
this.authClientPromise = Promise.resolve(authClient)
} else if (googleAuth) {
this.authClientPromise = Promise.resolve(googleAuth.getClient())
} else {
throw new Error('A `googleAuth` or `authClient` option is required.')
}
}
override validateHeaders(): void {
// Vertex auth headers are resolved asynchronously in prepareOptions.
}
override async prepareOptions(
options: Parameters<BaseAnthropic['prepareOptions']>[0],
): Promise<void> {
const authClient = await this.authClientPromise
const authHeaders = await authClient.getRequestHeaders()
const projectId =
authClient.projectId ?? getHeaderValue(authHeaders, 'x-goog-user-project')
if (!this.projectId && projectId) {
this.projectId = projectId
}
// Resolved Google auth headers MUST win: merge them last so a caller-supplied
// Authorization / x-goog-user-project can't override the Vertex credential
// and send the wrong token upstream. Other request headers still pass through.
options.headers = mergeHeaders(options.headers, authHeaders)
}
override async buildRequest(
options: Parameters<BaseAnthropic['buildRequest']>[0],
): ReturnType<BaseAnthropic['buildRequest']> {
if (isObj(options.body)) {
options.body = { ...options.body }
}
if (isObj(options.body) && !options.body['anthropic_version']) {
options.body['anthropic_version'] = DEFAULT_VERSION
}
if (MODEL_ENDPOINTS.has(options.path) && options.method === 'post') {
if (!this.projectId) {
throw new Error(
'No projectId was given and it could not be resolved from credentials. The client should be instantiated with the `projectId` option or the `ANTHROPIC_VERTEX_PROJECT_ID` environment variable should be set.',
)
}
if (!isObj(options.body)) {
throw new Error('Expected request body to be an object for post /v1/messages')
}
const model = options.body['model']
if (typeof model !== 'string' || model.trim() === '') {
throw new Error(
'Expected `model` to be a non-empty string for post /v1/messages',
)
}
delete options.body['model']
const stream = options.body['stream'] ?? false
const specifier = stream ? 'streamRawPredict' : 'rawPredict'
options.path = `/projects/${this.projectId}/locations/${this.region}/publishers/anthropic/models/${encodeURIComponent(model)}:${specifier}`
}
if (
options.method === 'post' &&
(options.path === '/v1/messages/count_tokens' ||
options.path === '/v1/messages/count_tokens?beta=true')
) {
if (!this.projectId) {
throw new Error(
'No projectId was given and it could not be resolved from credentials. The client should be instantiated with the `projectId` option or the `ANTHROPIC_VERTEX_PROJECT_ID` environment variable should be set.',
)
}
options.path = `/projects/${this.projectId}/locations/${this.region}/publishers/anthropic/models/count-tokens:rawPredict`
}
return super.buildRequest(options)
}
}
function makeMessagesResource(client: BaseAnthropic): Resources.Messages {
const resource = new Resources.Messages(client)
delete (resource as Partial<Resources.Messages>).batches
return resource
}
function makeBetaResource(client: BaseAnthropic): Resources.Beta {
const resource = new Resources.Beta(client)
delete (resource.messages as Partial<Resources.Beta.Messages>).batches
return resource
}