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:
Thomas Kosiewski 2025-08-08 16:40:23 +02:00
commit 56dc6d5fdb
No known key found for this signature in database
GPG key ID: 9C178FB21A57963E
12 changed files with 484 additions and 456 deletions

39
AGENTS.md Normal file
View 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 singlepurpose 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.

View file

@ -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

View file

@ -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

View file

@ -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=="],
}

View file

@ -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"
}
}

View file

@ -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;

View file

@ -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,
},
})
);

View file

@ -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;

View file

@ -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;
}

View file

@ -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,
};
}
}

View file

@ -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.

View file

@ -1,4 +1,4 @@
import { AISDKError } from "@ai-sdk/provider";
import { AISDKError } from 'ai';
const name = "AI_InvalidDataContentError";
const marker = `vercel.ai.error.${name}`;