diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..57a9438 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md index 810f854..0665b16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 - diff --git a/README.md b/README.md index d34d48d..89745b3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bun.lock b/bun.lock index c89d121..e733246 100644 --- a/bun.lock +++ b/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=="], } diff --git a/package.json b/package.json index 9b8edca..2f8417a 100644 --- a/package.json +++ b/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" } } diff --git a/src/anthropic-api-types.ts b/src/anthropic-api-types.ts index 76755e9..af56cc1 100644 --- a/src/anthropic-api-types.ts +++ b/src/anthropic-api-types.ts @@ -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; diff --git a/src/anthropic-proxy.ts b/src/anthropic-proxy.ts index b64c782..147e7b0 100644 --- a/src/anthropic-proxy.ts +++ b/src/anthropic-proxy.ts @@ -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; + providers: Record; 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, }, }) ); diff --git a/src/convert-anthropic-messages.ts b/src/convert-anthropic-messages.ts index 1804a21..a5d3339 100644 --- a/src/convert-anthropic-messages.ts +++ b/src/convert-anthropic-messages.ts @@ -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; @@ -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; + messages: Array; }; type AssistantBlock = { type: "assistant"; - messages: Array; + messages: Array; }; type UserBlock = { type: "user"; - messages: Array; + messages: Array; }; function groupIntoBlocks( - prompt: LanguageModelV1Prompt + prompt: LanguageModelV2Prompt ): Array { const blocks: Array = []; let currentBlock: SystemBlock | AssistantBlock | UserBlock | undefined = @@ -387,7 +358,7 @@ function groupIntoBlocks( export function convertFromAnthropicMessages( messages: ReadonlyArray ) { - const result: CoreMessage[] = []; + const result: ModelMessage[] = []; let toolCalls: Record = {}; 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; diff --git a/src/convert-to-anthropic-stream.ts b/src/convert-to-anthropic-stream.ts index 2712ba5..02a4c65 100644 --- a/src/convert-to-anthropic-stream.ts +++ b/src/convert-to-anthropic-stream.ts @@ -8,32 +8,30 @@ import { export function convertToAnthropicStream( stream: ReadableStream>> ): ReadableStream { + let index = 0; // content block index within the current message + const transform = new TransformStream< TextStreamPart>, 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; } diff --git a/src/convert-to-language-model-prompt.ts b/src/convert-to-language-model-prompt.ts index 4218eb2..eaaa8eb 100644 --- a/src/convert-to-language-model-prompt.ts +++ b/src/convert-to-language-model-prompt.ts @@ -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, }; } } diff --git a/src/data-content.ts b/src/data-content.ts index 4bf36da..76095df 100644 --- a/src/data-content.ts +++ b/src/data-content.ts @@ -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 = 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. diff --git a/src/invalid-data-content-error.ts b/src/invalid-data-content-error.ts index 85db045..5cc3f2e 100644 --- a/src/invalid-data-content-error.ts +++ b/src/invalid-data-content-error.ts @@ -1,4 +1,4 @@ -import { AISDKError } from "@ai-sdk/provider"; +import { AISDKError } from 'ai'; const name = "AI_InvalidDataContentError"; const marker = `vercel.ai.error.${name}`;