Add repository guidelines and update AI SDK to v2
Change-Id: Ib00b7ee5f39911b58a15674bc50700267018cdbc Signed-off-by: Thomas Kosiewski <tk@coder.com>
This commit is contained in:
parent
fce40fa5f7
commit
56dc6d5fdb
12 changed files with 484 additions and 456 deletions
39
AGENTS.md
Normal file
39
AGENTS.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `src/`: TypeScript sources. Key files: `main.ts` (CLI entry), `anthropic-proxy.ts` (HTTP proxy), `convert-*.ts` (format converters), `detect-mimetype.ts`, `json-schema.ts`.
|
||||
- `dist/`: Bundled CLI entry `main.js` (created by build).
|
||||
- `package.json`, `bun.lock`: Bun-based build and deps; `bin.anyclaude` points to `dist/main.js`.
|
||||
- `flake.nix`, `.envrc`: Nix/direnv developer shell and formatters.
|
||||
- Assets/config: `README.md`, `CLAUDE.md`, `tsconfig.json`, `demo.png`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Install: `bun install`.
|
||||
- Build: `bun run build` (outputs `dist/main.js` with Node shebang).
|
||||
- Run CLI (after build): `./dist/main.js --model openai/gpt-5-mini`.
|
||||
- Dev run (no build):
|
||||
- `PROXY_ONLY=true bun run src/main.ts` (prints proxy URL)
|
||||
- `OPENAI_API_KEY=... bun run src/main.ts --model openai/gpt-5-mini`
|
||||
- Nix shell: `direnv allow` (or `nix develop`); format Nix/shell files with `nix fmt`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Language: TypeScript (ESNext, strict mode enabled).
|
||||
- Indentation: 2 spaces; keep lines reasonable; use explicit imports (`verbatimModuleSyntax`).
|
||||
- Files: kebab-case `.ts` (e.g., `convert-to-anthropic-stream.ts`).
|
||||
- Names: `camelCase` for variables/functions; `PascalCase` for types/classes.
|
||||
- Exports: prefer named exports; keep modules single‑purpose and small.
|
||||
|
||||
## Testing Guidelines
|
||||
- No test runner is configured yet. If adding tests:
|
||||
- Place under `src/**/*.test.ts` or `src/__tests__/`.
|
||||
- Prioritize pure units (converters, schema, MIME detection). Avoid live provider calls by default; gate with env vars.
|
||||
- Add a `test` script in `package.json` and document how to run it in `README.md`.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits: imperative, concise subjects (e.g., "Add Nix development environment and Claude guidance file"). Include rationale in the body when helpful.
|
||||
- PRs: clear description, linked issues, commands used to verify (with relevant env vars), and expected behavior. Avoid committing secrets; scrub logs.
|
||||
- Keep scope small; update `README.md`/`CLAUDE.md` when behavior or env vars change.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Never commit API keys. Use `direnv` for local secrets and keep `.envrc` minimal.
|
||||
- Provider envs: `OPENAI_*`, `GOOGLE_*`, `XAI_*`, `AZURE_*`, optional `ANTHROPIC_*`. Use `PROXY_ONLY=true` to inspect the proxy without launching Claude.
|
||||
|
|
@ -48,7 +48,7 @@ Test the proxy manually:
|
|||
PROXY_ONLY=true bun run src/main.ts
|
||||
|
||||
# Test with a provider
|
||||
OPENAI_API_KEY=your-key bun run src/main.ts --model openai/gpt-4
|
||||
OPENAI_API_KEY=your-key bun run src/main.ts --model openai/gpt-5-mini
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
|
@ -64,4 +64,3 @@ Required for each provider:
|
|||
Special modes:
|
||||
|
||||
- `PROXY_ONLY=true`: Run proxy server without spawning Claude Code
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ $ pnpm install -g anyclaude
|
|||
|
||||
# anyclaude is a wrapper for the Claude CLI
|
||||
# `openai/`, `google/`, `xai/`, and `anthropic/` are supported
|
||||
$ anyclaude --model openai/o3
|
||||
$ anyclaude --model openai/gpt-5-mini
|
||||
```
|
||||
|
||||
Switch models in the Claude UI with `/model openai/o3`.
|
||||
Switch models in the Claude UI with `/model openai/gpt-5-mini`.
|
||||
|
||||
## FAQ
|
||||
|
||||
|
|
|
|||
61
bun.lock
61
bun.lock
|
|
@ -4,15 +4,16 @@
|
|||
"": {
|
||||
"name": "openclaude",
|
||||
"devDependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/azure": "^1.3.23",
|
||||
"@ai-sdk/google": "^1.2.18",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@ai-sdk/xai": "^1.2.16",
|
||||
"@ai-sdk/anthropic": "^2.0.1",
|
||||
"@ai-sdk/azure": "^2.0.6",
|
||||
"@ai-sdk/google": "^2.0.3",
|
||||
"@ai-sdk/openai": "^2.0.6",
|
||||
"@ai-sdk/xai": "^2.0.3",
|
||||
"@types/bun": "latest",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ai": "^4.3.16",
|
||||
"ai": "^5.0.8",
|
||||
"json-schema": "^0.4.0",
|
||||
"zod": "3.25.76",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
|
|
@ -20,67 +21,47 @@
|
|||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-HtNbpNV9qXQosHu00+CBMEcdTerwZY+kpVMNak0xP/P5TF6XkPf7IyizhLuc7y5zcXMjZCMA7jDGkcEdZCEdkw=="],
|
||||
|
||||
"@ai-sdk/azure": ["@ai-sdk/azure@1.3.23", "", { "dependencies": { "@ai-sdk/openai": "1.3.22", "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-vpsaPtU24RBVk/IMM5UylR/N4RtAuL2NZLWc7LJ3tvMTHu6pI46a7w+1qIwR3F6yO9ehWR8qvfLaBefJNFxaVw=="],
|
||||
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.6", "", { "dependencies": { "@ai-sdk/openai": "2.0.6", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-c8JbroE7B9Pyx3D8jxEwXop++vQqeR58adVrzPY7m29b/jZY7bsn6SuMheUF324sHbNJGd1DplqibIUzOs2qbA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@1.2.18", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-8B70+i+uB12Ae6Sn6B9Oc6W0W/XorGgc88Nx0pyUrcxFOdytHBaAVhTPqYsO3LLClfjYN8pQ9GMxd5cpGEnUcA=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@1.3.22", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-rQkY4nQGZnQw6pfftqsGsXXAqkGnl1gYjSbvD8+zyrVg2KAYeJU0NxIaacLWCORNcW+yQ/cqTpK/Uku/SIdEqA=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@0.2.14", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icjObfMCHKSIbywijaoLdZ1nSnuRnWgMEMLgwoxPJgxsUHMx0aVORnsLUid4SPtdhHI3X2masrt6iaEQLvOSFw=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.6", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-YmnhiyqllxnGo0Jo23jfi7NCOK+8BhUPPV/Cm/8MGFgI9oX/s6xGIp6KqSK2GFlAz2vxyy+aAlpt0cv/a0SxYQ=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-SE49TeOGppPtP4GTOXAl7WOhXXx42/VKpei3pNozjrD8NTkLX29Mib5t06VB5leaMjHNmyJMLECiWbWQV5W1ug=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
|
||||
|
||||
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||
|
||||
"@ai-sdk/xai": ["@ai-sdk/xai@1.2.16", "", { "dependencies": { "@ai-sdk/openai-compatible": "0.2.14", "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-UOZT8td9PWwMi2dF9a0U44t/Oltmf6QmIJdSvrOcLG4mvpRc1UJn6YJaR0HtXs3YnW6SvY1zRdIDrW4GFpv4NA=="],
|
||||
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.3", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.3", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-9Hsim5y4KJa+6DQBvoKcCp6ooWvfdbn1quOCEgSAnni6OJBL6QuImOcDmRr/YE6hrYo2RgLF56MtI2OyQqbh5w=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="],
|
||||
|
||||
"ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
|
||||
"ai": ["ai@5.0.8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
|
||||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||
"eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||
|
||||
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||
|
||||
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"zod": ["zod@3.25.30", "", {}, "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA=="],
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||
}
|
||||
|
|
|
|||
17
package.json
17
package.json
|
|
@ -14,15 +14,16 @@
|
|||
"anyclaude": "./dist/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/azure": "^1.3.23",
|
||||
"@ai-sdk/google": "^1.2.18",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@ai-sdk/xai": "^1.2.16",
|
||||
"@ai-sdk/anthropic": "^2.0.1",
|
||||
"@ai-sdk/azure": "^2.0.6",
|
||||
"@ai-sdk/google": "^2.0.3",
|
||||
"@ai-sdk/openai": "^2.0.6",
|
||||
"@ai-sdk/xai": "^2.0.3",
|
||||
"@types/bun": "latest",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ai": "^4.3.16",
|
||||
"json-schema": "^0.4.0"
|
||||
"ai": "^5.0.8",
|
||||
"json-schema": "^0.4.0",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
|
|
@ -30,6 +31,6 @@
|
|||
"description": "Run Claude Code with OpenAI, Google, xAI, and others.",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "bun build --target node --outfile dist/main.js ./src/main.ts --format cjs && sed -i '0,/^/s//#!\\/usr\\/bin\\/env node\\n/' ./dist/main.js"
|
||||
"build": "bun build --target node --outfile dist/main.js ./src/main.ts --format cjs --external ai --external @ai-sdk/* --external zod && node -e \"const fs=require('fs');const p='dist/main.js';fs.writeFileSync(p,'#!/usr/bin/env node\\n'+fs.readFileSync(p,'utf8'))\" && chmod +x dist/main.js"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ export interface AnthropicTextContent {
|
|||
export interface AnthropicThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
signature: string;
|
||||
// signature is optional; not all providers expose it
|
||||
signature?: string;
|
||||
cache_control: AnthropicCacheControl | undefined;
|
||||
}
|
||||
|
||||
|
|
@ -51,14 +52,14 @@ export interface AnthropicRedactedThinkingContent {
|
|||
|
||||
type AnthropicContentSource =
|
||||
| {
|
||||
type: "base64";
|
||||
media_type: string;
|
||||
data: string;
|
||||
}
|
||||
type: "base64";
|
||||
media_type: string;
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
type: "url";
|
||||
url: string;
|
||||
};
|
||||
type: "url";
|
||||
url: string;
|
||||
};
|
||||
|
||||
export interface AnthropicImageContent {
|
||||
type: "image";
|
||||
|
|
@ -90,25 +91,25 @@ export interface AnthropicToolResultContent {
|
|||
|
||||
export type AnthropicTool =
|
||||
| {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
input_schema: JSONSchema7;
|
||||
}
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
input_schema: JSONSchema7;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: "computer_20250124" | "computer_20241022";
|
||||
display_width_px: number;
|
||||
display_height_px: number;
|
||||
display_number: number;
|
||||
}
|
||||
name: string;
|
||||
type: "computer_20250124" | "computer_20241022";
|
||||
display_width_px: number;
|
||||
display_height_px: number;
|
||||
display_number: number;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: "text_editor_20250124" | "text_editor_20241022";
|
||||
}
|
||||
name: string;
|
||||
type: "text_editor_20250124" | "text_editor_20241022";
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: "bash_20250124" | "bash_20241022";
|
||||
};
|
||||
name: string;
|
||||
type: "bash_20250124" | "bash_20241022";
|
||||
};
|
||||
|
||||
export type AnthropicToolChoice =
|
||||
| { type: "auto" | "any" }
|
||||
|
|
@ -121,65 +122,65 @@ export type AnthropicStreamUsage = {
|
|||
|
||||
export type AnthropicStreamChunk =
|
||||
| {
|
||||
type: "message_start";
|
||||
message: AnthropicAssistantMessage & {
|
||||
id: string;
|
||||
model: string;
|
||||
stop_reason: string | null;
|
||||
stop_sequence: string | null;
|
||||
usage: AnthropicStreamUsage;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "content_block_start";
|
||||
index: number;
|
||||
content_block:
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "tool_use";
|
||||
id: string;
|
||||
name: string;
|
||||
input: any;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "content_block_delta";
|
||||
index: number;
|
||||
delta:
|
||||
| {
|
||||
type: "text_delta";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "input_json_delta";
|
||||
partial_json: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "content_block_stop";
|
||||
index: number;
|
||||
}
|
||||
| {
|
||||
type: "message_delta";
|
||||
delta: {
|
||||
stop_reason: string;
|
||||
stop_sequence: string | null;
|
||||
};
|
||||
type: "message_start";
|
||||
message: AnthropicAssistantMessage & {
|
||||
id: string;
|
||||
model: string;
|
||||
stop_reason: string | null;
|
||||
stop_sequence: string | null;
|
||||
usage: AnthropicStreamUsage;
|
||||
}
|
||||
| {
|
||||
type: "message_stop";
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
error: {
|
||||
type: "api_error";
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "content_block_start";
|
||||
index: number;
|
||||
content_block:
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "tool_use";
|
||||
id: string;
|
||||
name: string;
|
||||
input: any;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "content_block_delta";
|
||||
index: number;
|
||||
delta:
|
||||
| {
|
||||
type: "text_delta";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "input_json_delta";
|
||||
partial_json: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "content_block_stop";
|
||||
index: number;
|
||||
}
|
||||
| {
|
||||
type: "message_delta";
|
||||
delta: {
|
||||
stop_reason: string;
|
||||
stop_sequence: string | null;
|
||||
};
|
||||
usage: AnthropicStreamUsage;
|
||||
}
|
||||
| {
|
||||
type: "message_stop";
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
error: {
|
||||
type: "api_error";
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AnthropicMessagesRequest = {
|
||||
model: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ProviderV1 } from "@ai-sdk/provider";
|
||||
import type { ProviderV2 } from "@ai-sdk/provider";
|
||||
import { jsonSchema, streamText, type Tool } from "ai";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
|
|
@ -13,7 +13,7 @@ import { convertToLanguageModelMessage } from "./convert-to-language-model-promp
|
|||
import { providerizeSchema } from "./json-schema";
|
||||
|
||||
export type CreateAnthropicProxyOptions = {
|
||||
providers: Record<string, ProviderV1>;
|
||||
providers: Record<string, ProviderV2>;
|
||||
port?: number;
|
||||
};
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ export const createAnthropicProxy = ({
|
|||
const tools = body.tools?.reduce((acc, tool) => {
|
||||
acc[tool.name] = {
|
||||
description: tool.name,
|
||||
parameters: jsonSchema(
|
||||
inputSchema: jsonSchema(
|
||||
providerizeSchema(providerName, tool.input_schema)
|
||||
),
|
||||
};
|
||||
|
|
@ -133,7 +133,7 @@ export const createAnthropicProxy = ({
|
|||
system,
|
||||
tools,
|
||||
messages: coreMessages,
|
||||
maxTokens: body.max_tokens,
|
||||
maxOutputTokens: body.max_tokens,
|
||||
temperature: body.temperature,
|
||||
|
||||
onFinish: ({ response, usage, finishReason }) => {
|
||||
|
|
@ -161,7 +161,7 @@ export const createAnthropicProxy = ({
|
|||
|
||||
res.writeHead(200, { "Content-Type": "application/json" }).end(
|
||||
JSON.stringify({
|
||||
id: message.id,
|
||||
id: "msg_" + Date.now(),
|
||||
type: "message",
|
||||
role: promptMessage.role,
|
||||
content: promptMessage.content,
|
||||
|
|
@ -169,8 +169,8 @@ export const createAnthropicProxy = ({
|
|||
stop_reason: mapAnthropicStopReason(finishReason),
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: usage.promptTokens,
|
||||
output_tokens: usage.completionTokens,
|
||||
input_tokens: usage.inputTokens,
|
||||
output_tokens: usage.outputTokens,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
type LanguageModelV1CallWarning,
|
||||
type LanguageModelV1Message,
|
||||
type LanguageModelV1Prompt,
|
||||
type LanguageModelV1ProviderMetadata,
|
||||
type LanguageModelV2CallWarning,
|
||||
type LanguageModelV2Message,
|
||||
type LanguageModelV2Prompt,
|
||||
type SharedV2ProviderOptions,
|
||||
UnsupportedFunctionalityError,
|
||||
} from "@ai-sdk/provider";
|
||||
import { convertUint8ArrayToBase64 } from "@ai-sdk/provider-utils";
|
||||
|
|
@ -12,18 +12,19 @@ import type {
|
|||
AnthropicMessage,
|
||||
AnthropicMessagesPrompt,
|
||||
AnthropicUserMessage,
|
||||
AnthropicToolResultContent,
|
||||
} from "./anthropic-api-types";
|
||||
import type { CoreMessage, FilePart, TextPart, ToolCallPart } from "ai";
|
||||
import type { ReasoningUIPart } from "@ai-sdk/ui-utils";
|
||||
import type { ModelMessage, FilePart, TextPart, ToolCallPart } from "ai";
|
||||
import type { ReasoningUIPart } from 'ai';
|
||||
|
||||
export function convertToAnthropicMessagesPrompt({
|
||||
prompt,
|
||||
sendReasoning,
|
||||
warnings,
|
||||
}: {
|
||||
prompt: LanguageModelV1Prompt;
|
||||
prompt: LanguageModelV2Prompt;
|
||||
sendReasoning: boolean;
|
||||
warnings: LanguageModelV1CallWarning[];
|
||||
warnings: LanguageModelV2CallWarning[];
|
||||
}): {
|
||||
prompt: AnthropicMessagesPrompt;
|
||||
betas: Set<string>;
|
||||
|
|
@ -35,9 +36,9 @@ export function convertToAnthropicMessagesPrompt({
|
|||
const messages: AnthropicMessagesPrompt["messages"] = [];
|
||||
|
||||
function getCacheControl(
|
||||
providerMetadata: LanguageModelV1ProviderMetadata | undefined
|
||||
providerOptions: SharedV2ProviderOptions | undefined
|
||||
): AnthropicCacheControl | undefined {
|
||||
const anthropic = providerMetadata?.anthropic;
|
||||
const anthropic = providerOptions?.anthropic;
|
||||
|
||||
// allow both cacheControl and cache_control:
|
||||
const cacheControlValue =
|
||||
|
|
@ -62,10 +63,10 @@ export function convertToAnthropicMessagesPrompt({
|
|||
});
|
||||
}
|
||||
|
||||
system = block.messages.map(({ content, providerMetadata }) => ({
|
||||
system = block.messages.map(({ content, providerOptions }) => ({
|
||||
type: "text",
|
||||
text: content,
|
||||
cache_control: getCacheControl(providerMetadata),
|
||||
cache_control: getCacheControl(providerOptions),
|
||||
}));
|
||||
|
||||
break;
|
||||
|
|
@ -76,139 +77,119 @@ export function convertToAnthropicMessagesPrompt({
|
|||
const anthropicContent: AnthropicUserMessage["content"] = [];
|
||||
|
||||
for (const message of block.messages) {
|
||||
const { role, content } = message;
|
||||
switch (role) {
|
||||
case "user": {
|
||||
for (let j = 0; j < content.length; j++) {
|
||||
const part = content[j]!;
|
||||
if (message.role === "user") {
|
||||
const content = message.content;
|
||||
for (let j = 0; j < content.length; j++) {
|
||||
const part = content[j]!;
|
||||
|
||||
// cache control: first add cache control from part.
|
||||
// for the last part of a message,
|
||||
// check also if the message has cache control.
|
||||
const isLastPart = j === content.length - 1;
|
||||
|
||||
const cacheControl =
|
||||
getCacheControl(part.providerMetadata) ??
|
||||
(isLastPart
|
||||
? getCacheControl(message.providerMetadata)
|
||||
: undefined);
|
||||
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
anthropicContent.push({
|
||||
type: "text",
|
||||
text: part.text,
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "image": {
|
||||
anthropicContent.push({
|
||||
type: "image",
|
||||
source:
|
||||
part.image instanceof URL
|
||||
? {
|
||||
type: "url",
|
||||
url: part.image.toString(),
|
||||
}
|
||||
: {
|
||||
type: "base64",
|
||||
media_type: part.mimeType ?? "image/jpeg",
|
||||
data: convertUint8ArrayToBase64(part.image),
|
||||
},
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "file": {
|
||||
if (part.mimeType !== "application/pdf") {
|
||||
throw new UnsupportedFunctionalityError({
|
||||
functionality: "Non-PDF files in user messages",
|
||||
});
|
||||
}
|
||||
|
||||
betas.add("pdfs-2024-09-25");
|
||||
|
||||
anthropicContent.push({
|
||||
type: "document",
|
||||
source:
|
||||
part.data instanceof URL
|
||||
? {
|
||||
type: "url",
|
||||
url: part.data.toString(),
|
||||
}
|
||||
: {
|
||||
type: "base64",
|
||||
media_type: "application/pdf",
|
||||
data: part.data,
|
||||
},
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "tool": {
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const part = content[i]!;
|
||||
|
||||
// cache control: first add cache control from part.
|
||||
// for the last part of a message,
|
||||
// check also if the message has cache control.
|
||||
const isLastPart = i === content.length - 1;
|
||||
|
||||
const cacheControl =
|
||||
getCacheControl(part.providerMetadata) ??
|
||||
(isLastPart
|
||||
? getCacheControl(message.providerMetadata)
|
||||
: undefined);
|
||||
|
||||
const toolResultContent =
|
||||
part.content != null
|
||||
? part.content.map((part) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: part.text,
|
||||
cache_control: undefined,
|
||||
};
|
||||
case "image":
|
||||
return {
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: "base64" as const,
|
||||
media_type: part.mimeType ?? "image/jpeg",
|
||||
data: part.data,
|
||||
},
|
||||
cache_control: undefined,
|
||||
};
|
||||
}
|
||||
})
|
||||
: JSON.stringify(part.result);
|
||||
const isLastPart = j === content.length - 1;
|
||||
const cacheControl =
|
||||
getCacheControl(part.providerOptions) ??
|
||||
(isLastPart ? getCacheControl(message.providerOptions) : undefined);
|
||||
|
||||
if (part.type === "text") {
|
||||
anthropicContent.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: part.toolCallId,
|
||||
content: toolResultContent,
|
||||
is_error: part.isError,
|
||||
type: "text",
|
||||
text: part.text,
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
} else if (part.type === "file") {
|
||||
const mediaType = part.mediaType;
|
||||
if (mediaType === "application/pdf") {
|
||||
betas.add("pdfs-2024-09-25");
|
||||
anthropicContent.push({
|
||||
type: "document",
|
||||
source:
|
||||
part.data instanceof URL
|
||||
? { type: "url", url: part.data.toString() }
|
||||
: {
|
||||
type: "base64",
|
||||
media_type: "application/pdf",
|
||||
data: typeof part.data === "string"
|
||||
? part.data
|
||||
: convertUint8ArrayToBase64(part.data),
|
||||
},
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
} else if (mediaType?.startsWith("image/")) {
|
||||
anthropicContent.push({
|
||||
type: "image",
|
||||
source:
|
||||
part.data instanceof URL
|
||||
? { type: "url", url: part.data.toString() }
|
||||
: {
|
||||
type: "base64",
|
||||
media_type: mediaType ?? "image/jpeg",
|
||||
data: typeof part.data === "string"
|
||||
? part.data
|
||||
: convertUint8ArrayToBase64(part.data),
|
||||
},
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
} else {
|
||||
throw new UnsupportedFunctionalityError({
|
||||
functionality: `Unsupported user file media type: ${mediaType}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.role === "tool") {
|
||||
const content = message.content;
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const part = content[i]!;
|
||||
|
||||
const isLastPart = i === content.length - 1;
|
||||
const cacheControl =
|
||||
getCacheControl(part.providerOptions) ??
|
||||
(isLastPart ? getCacheControl(message.providerOptions) : undefined);
|
||||
|
||||
// Map LanguageModelV2ToolResultPart.output to Anthropic tool_result content
|
||||
let toolResultContent: AnthropicToolResultContent["content"];
|
||||
let isError: boolean | undefined;
|
||||
|
||||
switch (part.output.type) {
|
||||
case "text":
|
||||
toolResultContent = part.output.value;
|
||||
isError = false;
|
||||
break;
|
||||
case "json":
|
||||
toolResultContent = JSON.stringify(part.output.value);
|
||||
isError = false;
|
||||
break;
|
||||
case "error-text":
|
||||
toolResultContent = part.output.value;
|
||||
isError = true;
|
||||
break;
|
||||
case "error-json":
|
||||
toolResultContent = JSON.stringify(part.output.value);
|
||||
isError = true;
|
||||
break;
|
||||
case "content":
|
||||
toolResultContent = part.output.value.map((c) =>
|
||||
c.type === "text"
|
||||
? { type: "text" as const, text: c.text, cache_control: undefined }
|
||||
: c.mediaType === "application/pdf"
|
||||
? { type: "text" as const, text: "[document content omitted]", cache_control: undefined }
|
||||
: {
|
||||
type: "image" as const,
|
||||
source: { type: "base64" as const, media_type: c.mediaType, data: c.data },
|
||||
cache_control: undefined,
|
||||
}
|
||||
);
|
||||
isError = false;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = role;
|
||||
throw new Error(`Unsupported role: ${_exhaustiveCheck}`);
|
||||
anthropicContent.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: part.toolCallId,
|
||||
content: toolResultContent,
|
||||
is_error: isError,
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported role in user block`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,9 +215,9 @@ export function convertToAnthropicMessagesPrompt({
|
|||
// for the last part of a message,
|
||||
// check also if the message has cache control.
|
||||
const cacheControl =
|
||||
getCacheControl(part.providerMetadata) ??
|
||||
getCacheControl(part.providerOptions) ??
|
||||
(isLastContentPart
|
||||
? getCacheControl(message.providerMetadata)
|
||||
? getCacheControl(message.providerOptions)
|
||||
: undefined);
|
||||
|
||||
switch (part.type) {
|
||||
|
|
@ -261,7 +242,6 @@ export function convertToAnthropicMessagesPrompt({
|
|||
anthropicContent.push({
|
||||
type: "thinking",
|
||||
thinking: part.text,
|
||||
signature: part.signature!,
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
} else {
|
||||
|
|
@ -274,21 +254,12 @@ export function convertToAnthropicMessagesPrompt({
|
|||
break;
|
||||
}
|
||||
|
||||
case "redacted-reasoning": {
|
||||
anthropicContent.push({
|
||||
type: "redacted_thinking",
|
||||
data: part.data,
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
anthropicContent.push({
|
||||
type: "tool_use",
|
||||
id: part.toolCallId,
|
||||
name: part.toolName,
|
||||
input: part.args,
|
||||
input: part.input,
|
||||
cache_control: cacheControl,
|
||||
});
|
||||
break;
|
||||
|
|
@ -317,19 +288,19 @@ export function convertToAnthropicMessagesPrompt({
|
|||
|
||||
type SystemBlock = {
|
||||
type: "system";
|
||||
messages: Array<LanguageModelV1Message & { role: "system" }>;
|
||||
messages: Array<LanguageModelV2Message & { role: "system" }>;
|
||||
};
|
||||
type AssistantBlock = {
|
||||
type: "assistant";
|
||||
messages: Array<LanguageModelV1Message & { role: "assistant" }>;
|
||||
messages: Array<LanguageModelV2Message & { role: "assistant" }>;
|
||||
};
|
||||
type UserBlock = {
|
||||
type: "user";
|
||||
messages: Array<LanguageModelV1Message & { role: "user" | "tool" }>;
|
||||
messages: Array<LanguageModelV2Message & { role: "user" | "tool" }>;
|
||||
};
|
||||
|
||||
function groupIntoBlocks(
|
||||
prompt: LanguageModelV1Prompt
|
||||
prompt: LanguageModelV2Prompt
|
||||
): Array<SystemBlock | AssistantBlock | UserBlock> {
|
||||
const blocks: Array<SystemBlock | AssistantBlock | UserBlock> = [];
|
||||
let currentBlock: SystemBlock | AssistantBlock | UserBlock | undefined =
|
||||
|
|
@ -387,7 +358,7 @@ function groupIntoBlocks(
|
|||
export function convertFromAnthropicMessages(
|
||||
messages: ReadonlyArray<AnthropicMessage>
|
||||
) {
|
||||
const result: CoreMessage[] = [];
|
||||
const result: ModelMessage[] = [];
|
||||
let toolCalls: Record<string, ToolCallPart> = {};
|
||||
|
||||
for (const message of messages) {
|
||||
|
|
@ -411,16 +382,16 @@ export function convertFromAnthropicMessages(
|
|||
case "tool_use": {
|
||||
messageContent.push({
|
||||
type: "tool-call",
|
||||
args: content.input,
|
||||
input: content.input,
|
||||
toolCallId: content.id,
|
||||
toolName: content.name,
|
||||
});
|
||||
toolCalls[content.id] = {
|
||||
type: "tool-call",
|
||||
args: content.input,
|
||||
input: content.input,
|
||||
toolCallId: content.id,
|
||||
toolName: content.name,
|
||||
};
|
||||
} as ToolCallPart;
|
||||
break;
|
||||
}
|
||||
case "tool_result": {
|
||||
|
|
@ -428,11 +399,41 @@ export function convertFromAnthropicMessages(
|
|||
if (!toolCall) {
|
||||
throw new Error("Tool call not found");
|
||||
}
|
||||
// Map Anthropic tool_result content to LanguageModelV2ToolResultOutput
|
||||
const output = ((): any => {
|
||||
const c = content.content;
|
||||
if (typeof c === "string") {
|
||||
return { type: "text", value: c } as const;
|
||||
}
|
||||
// array of text/image contents
|
||||
const mapped = c.map((p) => {
|
||||
if (p.type === "text") {
|
||||
return { type: "text" as const, text: p.text };
|
||||
}
|
||||
// image content: prefer base64 source
|
||||
if (p.type === "image") {
|
||||
if (p.source.type === "base64") {
|
||||
return {
|
||||
type: "media" as const,
|
||||
data: p.source.data,
|
||||
mediaType: p.source.media_type,
|
||||
};
|
||||
} else {
|
||||
// URL fallback: encode as text with URL
|
||||
return { type: "text" as const, text: p.source.url };
|
||||
}
|
||||
}
|
||||
// default to text
|
||||
return { type: "text" as const, text: "" };
|
||||
});
|
||||
return { type: "content", value: mapped } as const;
|
||||
})();
|
||||
|
||||
result.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
result: content.content,
|
||||
output,
|
||||
toolCallId: content.tool_use_id,
|
||||
toolName: toolCall.toolName,
|
||||
type: "tool-result",
|
||||
|
|
@ -454,7 +455,7 @@ export function convertFromAnthropicMessages(
|
|||
result.push({
|
||||
role: message.role,
|
||||
content: messageContent,
|
||||
} as CoreMessage);
|
||||
} as ModelMessage);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -8,32 +8,30 @@ import {
|
|||
export function convertToAnthropicStream(
|
||||
stream: ReadableStream<TextStreamPart<Record<string, Tool>>>
|
||||
): ReadableStream<AnthropicStreamChunk> {
|
||||
let index = 0; // content block index within the current message
|
||||
|
||||
const transform = new TransformStream<
|
||||
TextStreamPart<Record<string, Tool>>,
|
||||
AnthropicStreamChunk
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
let index = 0;
|
||||
|
||||
switch (chunk.type) {
|
||||
case "step-start":
|
||||
case "start-step": {
|
||||
controller.enqueue({
|
||||
type: "message_start",
|
||||
message: {
|
||||
id: chunk.messageId,
|
||||
id: "msg_" + Date.now(),
|
||||
role: "assistant",
|
||||
content: [],
|
||||
model: "claude-4-sonnet-20250514",
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
},
|
||||
usage: { input_tokens: 0, output_tokens: 0 },
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "step-finish":
|
||||
}
|
||||
case "finish-step": {
|
||||
controller.enqueue({
|
||||
type: "message_delta",
|
||||
delta: {
|
||||
|
|
@ -41,63 +39,81 @@ export function convertToAnthropicStream(
|
|||
stop_sequence: null,
|
||||
},
|
||||
usage: {
|
||||
input_tokens: chunk.usage.promptTokens,
|
||||
output_tokens: chunk.usage.completionTokens,
|
||||
},
|
||||
});
|
||||
index++;
|
||||
break;
|
||||
case "finish":
|
||||
controller.enqueue({
|
||||
type: "message_stop",
|
||||
});
|
||||
break;
|
||||
case "text-delta":
|
||||
controller.enqueue({
|
||||
type: "content_block_delta",
|
||||
index: index,
|
||||
delta: {
|
||||
type: "text_delta",
|
||||
text: chunk.textDelta,
|
||||
input_tokens: chunk.usage.inputTokens ?? 0,
|
||||
output_tokens: chunk.usage.outputTokens ?? 0,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "tool-call-streaming-start":
|
||||
}
|
||||
case "finish": {
|
||||
controller.enqueue({ type: "message_stop" });
|
||||
// reset index for next message
|
||||
index = 0;
|
||||
break;
|
||||
}
|
||||
case "text-start": {
|
||||
controller.enqueue({
|
||||
type: "content_block_start",
|
||||
index: index,
|
||||
index,
|
||||
content_block: { type: "text", text: "" },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "text-delta": {
|
||||
controller.enqueue({
|
||||
type: "content_block_delta",
|
||||
index,
|
||||
delta: { type: "text_delta", text: chunk.text },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "text-end": {
|
||||
controller.enqueue({ type: "content_block_stop", index });
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "tool-input-start": {
|
||||
controller.enqueue({
|
||||
type: "content_block_start",
|
||||
index,
|
||||
content_block: {
|
||||
type: "tool_use",
|
||||
id: chunk.toolCallId,
|
||||
id: chunk.id,
|
||||
name: chunk.toolName,
|
||||
input: {},
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "tool-call-delta":
|
||||
}
|
||||
case "tool-input-delta": {
|
||||
controller.enqueue({
|
||||
type: "content_block_delta",
|
||||
index: index,
|
||||
delta: {
|
||||
type: "input_json_delta",
|
||||
partial_json: chunk.argsTextDelta,
|
||||
},
|
||||
index,
|
||||
delta: { type: "input_json_delta", partial_json: chunk.delta },
|
||||
});
|
||||
break;
|
||||
case "tool-call":
|
||||
}
|
||||
case "tool-input-end": {
|
||||
controller.enqueue({ type: "content_block_stop", index });
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "tool-call": {
|
||||
controller.enqueue({
|
||||
type: "content_block_start",
|
||||
index: index,
|
||||
index,
|
||||
content_block: {
|
||||
type: "tool_use",
|
||||
id: chunk.toolCallId,
|
||||
name: chunk.toolName,
|
||||
input: chunk.args,
|
||||
input: (chunk as any).input,
|
||||
},
|
||||
});
|
||||
index++;
|
||||
controller.enqueue({ type: "content_block_stop", index });
|
||||
index += 1;
|
||||
break;
|
||||
case "error":
|
||||
}
|
||||
case "error": {
|
||||
controller.enqueue({
|
||||
type: "error",
|
||||
error: {
|
||||
|
|
@ -105,17 +121,29 @@ export function convertToAnthropicStream(
|
|||
message:
|
||||
chunk.error instanceof Error
|
||||
? chunk.error.message
|
||||
: chunk.error as string,
|
||||
: (chunk.error as string),
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
case "start":
|
||||
case "abort":
|
||||
case "raw":
|
||||
case "source":
|
||||
case "file":
|
||||
case "reasoning-start":
|
||||
case "reasoning-delta":
|
||||
case "reasoning-end":
|
||||
// ignore for Anthropic stream mapping
|
||||
break;
|
||||
default: {
|
||||
controller.error(new Error(`Unhandled chunk type: ${chunk.type}`));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
stream.pipeTo(transform.writable).catch((err) => {
|
||||
console.log("WE GOT AN ERROR");
|
||||
stream.pipeTo(transform.writable).catch(() => {
|
||||
// swallow propagation; error already forwarded via 'error' chunk
|
||||
});
|
||||
return transform.readable;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import type {
|
||||
LanguageModelV1FilePart,
|
||||
LanguageModelV1ImagePart,
|
||||
LanguageModelV1Message,
|
||||
LanguageModelV1TextPart,
|
||||
LanguageModelV2FilePart,
|
||||
LanguageModelV2Message,
|
||||
LanguageModelV2TextPart,
|
||||
LanguageModelV2ToolCallPart,
|
||||
LanguageModelV2ReasoningPart,
|
||||
} from "@ai-sdk/provider";
|
||||
import {
|
||||
InvalidMessageRoleError,
|
||||
type CoreMessage,
|
||||
type ModelMessage,
|
||||
type DataContent,
|
||||
type FilePart,
|
||||
type ImagePart,
|
||||
|
|
@ -27,20 +28,19 @@ import { splitDataUrl } from "./split-data-url";
|
|||
* available if the model does not support URLs, null otherwise.
|
||||
*/
|
||||
export function convertToLanguageModelMessage(
|
||||
message: CoreMessage,
|
||||
message: ModelMessage,
|
||||
downloadedAssets: Record<
|
||||
string,
|
||||
{ mimeType: string | undefined; data: Uint8Array }
|
||||
>
|
||||
): LanguageModelV1Message {
|
||||
): LanguageModelV2Message {
|
||||
const role = message.role;
|
||||
switch (role) {
|
||||
case "system": {
|
||||
return {
|
||||
role: "system",
|
||||
content: message.content,
|
||||
providerMetadata:
|
||||
message.providerOptions ?? message.experimental_providerMetadata,
|
||||
providerOptions: message.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -49,8 +49,7 @@ export function convertToLanguageModelMessage(
|
|||
return {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: message.content }],
|
||||
providerMetadata:
|
||||
message.providerOptions ?? message.experimental_providerMetadata,
|
||||
providerOptions: message.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +59,7 @@ export function convertToLanguageModelMessage(
|
|||
.map((part) => convertPartToLanguageModelPart(part, downloadedAssets))
|
||||
// remove empty text parts:
|
||||
.filter((part) => part.type !== "text" || part.text !== ""),
|
||||
providerMetadata:
|
||||
message.providerOptions ?? message.experimental_providerMetadata,
|
||||
providerOptions: message.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -70,70 +68,69 @@ export function convertToLanguageModelMessage(
|
|||
return {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: message.content }],
|
||||
providerMetadata:
|
||||
message.providerOptions ?? message.experimental_providerMetadata,
|
||||
providerOptions: message.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const assistantParts: Array<
|
||||
| LanguageModelV2TextPart
|
||||
| LanguageModelV2FilePart
|
||||
| LanguageModelV2ReasoningPart
|
||||
| LanguageModelV2ToolCallPart
|
||||
> = [];
|
||||
|
||||
for (const part of message.content) {
|
||||
if (part.type === "text" && part.text === "") continue;
|
||||
const providerOptions = part.providerOptions;
|
||||
switch (part.type) {
|
||||
case "file":
|
||||
assistantParts.push({
|
||||
type: "file",
|
||||
data:
|
||||
part.data instanceof URL
|
||||
? part.data
|
||||
: convertDataContentToBase64String(part.data),
|
||||
filename: part.filename,
|
||||
mediaType: part.mediaType,
|
||||
providerOptions,
|
||||
});
|
||||
break;
|
||||
case "reasoning":
|
||||
assistantParts.push({
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
providerOptions,
|
||||
});
|
||||
break;
|
||||
case "text":
|
||||
assistantParts.push({
|
||||
type: "text",
|
||||
text: part.text,
|
||||
providerOptions,
|
||||
});
|
||||
break;
|
||||
case "tool-call":
|
||||
assistantParts.push({
|
||||
type: "tool-call",
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
input: part.input,
|
||||
});
|
||||
break;
|
||||
case "tool-result":
|
||||
// ignore here; mapped in tool message case
|
||||
break;
|
||||
default: {
|
||||
const _never: never = part;
|
||||
void _never;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
role: "assistant",
|
||||
content: message.content
|
||||
.filter(
|
||||
// remove empty text parts:
|
||||
(part) => part.type !== "text" || part.text !== ""
|
||||
)
|
||||
.map((part) => {
|
||||
const providerOptions =
|
||||
part.providerOptions ?? part.experimental_providerMetadata;
|
||||
|
||||
switch (part.type) {
|
||||
case "file": {
|
||||
return {
|
||||
type: "file",
|
||||
data:
|
||||
part.data instanceof URL
|
||||
? part.data
|
||||
: convertDataContentToBase64String(part.data),
|
||||
filename: part.filename,
|
||||
mimeType: part.mimeType,
|
||||
providerMetadata: providerOptions,
|
||||
};
|
||||
}
|
||||
case "reasoning": {
|
||||
return {
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
signature: part.signature,
|
||||
providerMetadata: providerOptions,
|
||||
};
|
||||
}
|
||||
case "redacted-reasoning": {
|
||||
return {
|
||||
type: "redacted-reasoning",
|
||||
data: part.data,
|
||||
providerMetadata: providerOptions,
|
||||
};
|
||||
}
|
||||
case "text": {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: part.text,
|
||||
providerMetadata: providerOptions,
|
||||
};
|
||||
}
|
||||
case "tool-call": {
|
||||
return {
|
||||
type: "tool-call" as const,
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
args: part.args,
|
||||
providerMetadata: providerOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
providerMetadata:
|
||||
message.providerOptions ?? message.experimental_providerMetadata,
|
||||
content: assistantParts,
|
||||
providerOptions: message.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -144,14 +141,10 @@ export function convertToLanguageModelMessage(
|
|||
type: "tool-result",
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
result: part.result,
|
||||
content: part.experimental_content,
|
||||
isError: part.isError,
|
||||
providerMetadata:
|
||||
part.providerOptions ?? part.experimental_providerMetadata,
|
||||
providerOptions: part.providerOptions,
|
||||
output: part.output,
|
||||
})),
|
||||
providerMetadata:
|
||||
message.providerOptions ?? message.experimental_providerMetadata,
|
||||
providerOptions: message.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -177,19 +170,17 @@ function convertPartToLanguageModelPart(
|
|||
{ mimeType: string | undefined; data: Uint8Array }
|
||||
>
|
||||
):
|
||||
| LanguageModelV1TextPart
|
||||
| LanguageModelV1ImagePart
|
||||
| LanguageModelV1FilePart {
|
||||
| LanguageModelV2TextPart
|
||||
| LanguageModelV2FilePart {
|
||||
if (part.type === "text") {
|
||||
return {
|
||||
type: "text",
|
||||
text: part.text,
|
||||
providerMetadata:
|
||||
part.providerOptions ?? part.experimental_providerMetadata,
|
||||
providerOptions: part.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
let mimeType: string | undefined = part.mimeType;
|
||||
let mediaType = part.mediaType;
|
||||
let data: DataContent | URL;
|
||||
let content: URL | ArrayBuffer | string;
|
||||
let normalizedData: Uint8Array | URL;
|
||||
|
|
@ -228,7 +219,7 @@ function convertPartToLanguageModelPart(
|
|||
throw new Error(`Invalid data URL format in part ${type}`);
|
||||
}
|
||||
|
||||
mimeType = dataUrlMimeType;
|
||||
mediaType = dataUrlMimeType;
|
||||
normalizedData = convertDataContentToUint8Array(base64Content);
|
||||
} else {
|
||||
/**
|
||||
|
|
@ -239,7 +230,7 @@ function convertPartToLanguageModelPart(
|
|||
const downloadedFile = downloadedAssets[content.toString()];
|
||||
if (downloadedFile) {
|
||||
normalizedData = downloadedFile.data;
|
||||
mimeType ??= downloadedFile.mimeType;
|
||||
mediaType ??= downloadedFile.mimeType;
|
||||
} else {
|
||||
normalizedData = content;
|
||||
}
|
||||
|
|
@ -259,24 +250,23 @@ function convertPartToLanguageModelPart(
|
|||
// When detection fails, use provided mimetype.
|
||||
|
||||
if (normalizedData instanceof Uint8Array) {
|
||||
mimeType =
|
||||
mediaType =
|
||||
detectMimeType({
|
||||
data: normalizedData,
|
||||
signatures: imageMimeTypeSignatures,
|
||||
}) ?? mimeType;
|
||||
}) ?? mediaType;
|
||||
}
|
||||
return {
|
||||
type: "image",
|
||||
image: normalizedData,
|
||||
mimeType,
|
||||
providerMetadata:
|
||||
part.providerOptions ?? part.experimental_providerMetadata,
|
||||
type: "file",
|
||||
data: normalizedData,
|
||||
mediaType: mediaType ?? "",
|
||||
providerOptions: part.providerOptions,
|
||||
};
|
||||
}
|
||||
|
||||
case "file": {
|
||||
// We should have a mimeType at this point, if not, throw an error.
|
||||
if (mimeType == null) {
|
||||
if (mediaType == null) {
|
||||
throw new Error(`Mime type is missing for file part`);
|
||||
}
|
||||
|
||||
|
|
@ -287,9 +277,8 @@ function convertPartToLanguageModelPart(
|
|||
? convertDataContentToBase64String(normalizedData)
|
||||
: normalizedData,
|
||||
filename: part.filename,
|
||||
mimeType,
|
||||
providerMetadata:
|
||||
part.providerOptions ?? part.experimental_providerMetadata,
|
||||
mediaType: mediaType,
|
||||
providerOptions: part.providerOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
convertUint8ArrayToBase64,
|
||||
} from "@ai-sdk/provider-utils";
|
||||
import { InvalidDataContentError } from "./invalid-data-content-error";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
Data content. Can either be a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer.
|
||||
|
|
@ -13,17 +12,7 @@ export type DataContent = string | Uint8Array | ArrayBuffer | Buffer;
|
|||
/**
|
||||
@internal
|
||||
*/
|
||||
export const dataContentSchema: z.ZodType<DataContent> = z.union([
|
||||
z.string(),
|
||||
z.instanceof(Uint8Array),
|
||||
z.instanceof(ArrayBuffer),
|
||||
z.custom(
|
||||
// Buffer might not be available in some environments such as CloudFlare:
|
||||
(value: unknown): value is Buffer =>
|
||||
globalThis.Buffer?.isBuffer(value) ?? false,
|
||||
{ message: "Must be a Buffer" }
|
||||
),
|
||||
]);
|
||||
// Note: validate via runtime guards in converter functions; no Zod schema to avoid bundler TS resolution issues.
|
||||
|
||||
/**
|
||||
Converts data content to a base64-encoded string.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AISDKError } from "@ai-sdk/provider";
|
||||
import { AISDKError } from 'ai';
|
||||
|
||||
const name = "AI_InvalidDataContentError";
|
||||
const marker = `vercel.ai.error.${name}`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue