From e10ce7f53a224c9f8dd54b62ec5d2ae9a1bbbc74 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 21 May 2026 14:16:11 +0200 Subject: [PATCH 1/2] feat: add ClaudeCodeProvider for subscription users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps @anthropic-ai/claude-agent-sdk so Claude Pro/Max subscribers (who do not have a console API key) can use this library by authenticating through the local `claude` CLI. Subscribers run `claude login` once and the provider picks up the credentials automatically. Provider behavior: - query() is invoked in single-turn mode (maxTurns: 1, allowedTools: []) to behave as plain text completion by default. Both knobs are configurable for agentic use cases. - Conversation history is flattened to a single prompt with role labels. - doStream() emits text deltas as the SDK yields successive assistant messages. doComplete() accumulates and returns the final result. - SDKAssistantMessageError codes (authentication_failed, billing_error, rate_limit, model_not_found, ...) map to AIErrorType variants. - ClaudeCodeConfig.apiKey is optional — when omitted the SDK falls back to ANTHROPIC_API_KEY or local subscription credentials. The base validator is overridden to allow this. Tradeoffs: - Requires the `claude` CLI installed and logged in on the host. This is not suitable for typical server-side production deployments; it is the official path for subscription accounts. - Higher latency (CLI process spawn) than the direct Anthropic API. - The agent SDK is heavy. Bundle grows ~750KB; bun build now uses --target node so the SDK's Node built-in imports resolve correctly. Wired into PROVIDER_REGISTRY ('claude-code'), createClaudeCodeProvider, SUPPORTED_PROVIDERS, and re-exported from the package entry. --- bun.lock | 199 +++++++++++++++++++ package.json | 5 +- src/constants.ts | 3 +- src/index.ts | 4 +- src/providers/claude-code.ts | 371 +++++++++++++++++++++++++++++++++++ src/providers/index.ts | 3 +- src/utils/factory.ts | 11 +- 7 files changed, 590 insertions(+), 6 deletions(-) create mode 100644 src/providers/claude-code.ts diff --git a/bun.lock b/bun.lock index 37329a1..7eb7650 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "simple-ai-provider", "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.146", "@anthropic-ai/sdk": "^0.97.0", "@google/genai": "^2.5.0", "openai": "^6.0.0", @@ -19,12 +20,34 @@ }, }, "packages": { + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.146", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.146", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.146", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.146", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.146", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.146" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-hK9/Ng+hOyexUemTxdIUsSWJ9o2LFi2YNWzHwz8/YMCohUYOnFMZkBiENvUAb0WIc5hieOyBZrOIlg5OewuJMg=="], + + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.146", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0IIvlEaenq2CRSVx5Bo5BaCtHQXS87GancM35WKEYveGVLn6DI+5G7ikYuTE4AKRPkMnogFtY4BJt6LulWGj+A=="], + + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.146", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dk5xJ03Ff1JXbMRP1t2wc/TyfY6xF/2Ysp31wMhFPjoNiKSPHMWaIg242+T3CHdxLWmJ8plWHL1HL5cyZ/LCkw=="], + + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.146", "", { "os": "linux", "cpu": "arm64" }, "sha512-mzBXDDWWBAC/vDtAYpO1G/dq5QvJtYSPXsqcb+sNdcDhiuf4IYnYp7ytRncYlsUNDkLmX6Gk2jkWAHUUA2Lozg=="], + + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.146", "", { "os": "linux", "cpu": "arm64" }, "sha512-QlCid0ucdrmhUAOewfQjaofN2wlokWcfFTxSFePTSj1umk35JO7TDFP700F7jU49r1fPWIdvJpPwWGyB0DeFPA=="], + + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.146", "", { "os": "linux", "cpu": "x64" }, "sha512-B2baXU1tCBT5CVlD7jJMKjpC4xdO45NUIWpqImmwuOfKvlM/PITjyTXyTY662mGZf1dBmdqBBsqirwFH/jhi8Q=="], + + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.146", "", { "os": "linux", "cpu": "x64" }, "sha512-E3coK1ThQT08KIX80RLcsq7DWXFllCKOzoOe32it/bdtY56TBgPY9xemwXhIJ+cVBHTI9/MpBSIlKBcFCt+yQA=="], + + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.146", "", { "os": "win32", "cpu": "arm64" }, "sha512-CIwQxGX2r/yWpjCJ6ahB3smKXhghWgGTxL98+LGW52TUwqTiBnlNrH9DPqqgv1/+Hyquw6xfLrKU+StyfMgiLw=="], + + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.146", "", { "os": "win32", "cpu": "x64" }, "sha512-qmxrsyaqA8s4HShqJls7ZCRjdoqN66Jo/hbjQNB3uHepD8tEO1iD19aPV4+osdLT7feMkhDBfLT07Q30R2NB5w=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.97.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg=="], "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@google/genai": ["@google/genai@2.5.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-qDi3LLh9I3llJK0f9uV8kZ8EdT9oHPxGJJ9yOJ/i5YXYrVwRCs8jHo9x4e99uOeKYDvD3TZwT70p/H/LS3BixQ=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -53,80 +76,256 @@ "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hono": ["hono@4.12.21", "", {}, "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="], "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "protobufjs": ["protobufjs@7.6.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-LtESOsMPTZgyYtwxhvdgdjGL0HmXEaRA/hVD6sol4zA60hVXXXP/SGmxnqDbgGE8gy7pYex7cym+5vYPcmaXBQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "bun-types/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/package.json b/package.json index d093d49..41cba98 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs", "build:clean": "rm -rf dist", "build:types": "tsc --project tsconfig.build.json", - "build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm", - "build:cjs": "bun build src/index.ts --outfile dist/index.js --format cjs", + "build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm --target node", + "build:cjs": "bun build src/index.ts --outfile dist/index.js --format cjs --target node", "dev": "bun run src/index.ts", "test": "bun test", "prepublishOnly": "bun run build" @@ -52,6 +52,7 @@ "url": "https://gitea.jleibl.net/jleibl/simple-ai-provider.git" }, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.146", "@anthropic-ai/sdk": "^0.97.0", "@google/genai": "^2.5.0", "openai": "^6.0.0" diff --git a/src/constants.ts b/src/constants.ts index 7cdd2c7..978b31e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,7 +16,8 @@ export const DEFAULT_MODELS = { claude: 'claude-3-5-sonnet-20241022', openai: 'gpt-4o', gemini: 'gemini-2.5-flash', - openwebui: 'llama3.1:latest' + openwebui: 'llama3.1:latest', + 'claude-code': 'sonnet' } as const; export const DEFAULT_OPENWEBUI_BASE_URL = 'http://localhost:3000'; diff --git a/src/index.ts b/src/index.ts index 369e6ec..0ec12ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export { ClaudeProvider, type ClaudeConfig } from './providers/claude.js'; export { OpenAIProvider, type OpenAIConfig } from './providers/openai.js'; export { GeminiProvider, type GeminiConfig } from './providers/gemini.js'; export { OpenWebUIProvider, type OpenWebUIConfig } from './providers/openwebui.js'; +export { ClaudeCodeProvider, type ClaudeCodeConfig } from './providers/claude-code.js'; // Factory utilities export { @@ -46,6 +47,7 @@ export { createOpenAIProvider, createGeminiProvider, createOpenWebUIProvider, + createClaudeCodeProvider, type ProviderType, PROVIDER_REGISTRY } from './utils/factory.js'; @@ -53,7 +55,7 @@ export { /** * List of all supported providers */ -export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui'] as const; +export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui', 'claude-code'] as const; /** * Package version diff --git a/src/providers/claude-code.ts b/src/providers/claude-code.ts new file mode 100644 index 0000000..d5f0303 --- /dev/null +++ b/src/providers/claude-code.ts @@ -0,0 +1,371 @@ +/** + * Claude Code Provider + * + * Wraps `@anthropic-ai/claude-agent-sdk` so consumers can use a local + * Claude Code installation — including Claude Pro/Max subscription + * accounts authenticated via `claude login` — through the same + * BaseAIProvider interface as the other providers. + * + * Tradeoffs vs the direct Anthropic API provider: + * - Authenticates via the local CLI, so subscription users (no API key) + * can use it. + * - Requires `claude` to be installed and logged in on the host. + * - Higher latency (shells out to a CLI process per request). + * - Designed for agent workflows, but used here in single-turn mode + * (maxTurns: 1, no tools) for plain text completion. + * + * @see https://docs.anthropic.com/claude/docs/claude-code-sdk + */ + +import { query } from '@anthropic-ai/claude-agent-sdk'; +import type { + AIMessage, + AIProviderConfig, + CompletionChunk, + CompletionParams, + CompletionResponse, + ProviderInfo +} from '../types/index.js'; +import { BaseAIProvider } from './base.js'; +import { AIProviderError, AIErrorType } from '../types/index.js'; +import { DEFAULT_MODELS } from '../constants.js'; + +/** + * Configuration for the Claude Code provider. `apiKey` is optional — + * when omitted the SDK falls back to ANTHROPIC_API_KEY in the + * environment or the local subscription credentials managed by the + * `claude` CLI. + */ +export interface ClaudeCodeConfig extends Omit { + apiKey?: string; + + /** + * Default model. Accepts SDK aliases (`'sonnet'`, `'opus'`, `'haiku'`, + * `'inherit'`) or any full Claude model ID. + * @default 'sonnet' + */ + defaultModel?: string; + + /** + * Working directory for the Claude Code session. + * @default process.cwd() + */ + cwd?: string; + + /** + * Tool names the model is allowed to call. Defaults to none, which + * makes the provider behave as a pure text completion endpoint. + */ + allowedTools?: string[]; + + /** + * Maximum agent turns. Defaults to 1 for plain completion; raise this + * if you also pass `allowedTools` and want tool-use loops. + * @default 1 + */ + maxTurns?: number; +} + +export class ClaudeCodeProvider extends BaseAIProvider { + private readonly defaultModel: string; + private readonly cwd: string | undefined; + private readonly allowedTools: string[]; + private readonly maxTurns: number; + + constructor(config: ClaudeCodeConfig) { + super(config as AIProviderConfig); + + this.defaultModel = config.defaultModel || DEFAULT_MODELS['claude-code']; + this.cwd = config.cwd; + this.allowedTools = config.allowedTools ?? []; + this.maxTurns = config.maxTurns ?? 1; + } + + /** + * apiKey is optional for this provider — the SDK reads + * ANTHROPIC_API_KEY from the environment or falls back to the local + * Claude Code credentials. We override the base validator to skip the + * non-empty apiKey requirement. + */ + protected override validateAndNormalizeConfig(config: AIProviderConfig): AIProviderConfig { + if (!config) { + throw new AIProviderError( + 'Configuration object is required', + AIErrorType.INVALID_REQUEST + ); + } + + if (config.apiKey !== undefined && (typeof config.apiKey !== 'string')) { + throw new AIProviderError( + 'API key, when provided, must be a string', + AIErrorType.INVALID_REQUEST + ); + } + + return { + ...config, + apiKey: config.apiKey ?? '', + timeout: config.timeout, + maxRetries: config.maxRetries + }; + } + + protected async doInitialize(): Promise { + // The SDK lazy-spawns the CLI per query; no connection probe is + // necessary (and a probe would cost a real billed turn). We defer + // auth/model errors to the first complete()/stream() call. + if (this.config.apiKey) { + process.env.ANTHROPIC_API_KEY = this.config.apiKey; + } + } + + protected async doComplete(params: CompletionParams): Promise> { + const { systemPrompt, prompt } = this.buildPrompt(params.messages); + const model = params.model || this.defaultModel; + + let collectedText = ''; + let sessionId = ''; + let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }; + + for await (const message of query({ + prompt, + options: this.buildOptions(model, systemPrompt) + })) { + if (message.type === 'system' && message.subtype === 'init') { + sessionId = message.session_id; + continue; + } + + if (message.type === 'assistant') { + if (message.error) { + throw this.errorForSdkAssistantError(message.error); + } + collectedText += extractText(message.message.content); + continue; + } + + if (message.type === 'result') { + usage = { + promptTokens: message.usage?.input_tokens ?? 0, + completionTokens: message.usage?.output_tokens ?? 0, + totalTokens: (message.usage?.input_tokens ?? 0) + (message.usage?.output_tokens ?? 0) + }; + + if (message.subtype !== 'success') { + throw new AIProviderError( + `Claude Code session ended without a successful result: ${message.subtype}`, + AIErrorType.UNKNOWN + ); + } + + return { + content: collectedText || message.result, + model, + usage, + id: sessionId || message.uuid, + metadata: { + costUsd: message.total_cost_usd, + durationMs: message.duration_ms, + numTurns: message.num_turns, + stopReason: message.stop_reason + } + }; + } + } + + throw new AIProviderError( + 'Claude Code stream ended without a result message', + AIErrorType.UNKNOWN + ); + } + + protected async *doStream( + params: CompletionParams + ): AsyncIterable { + const { systemPrompt, prompt } = this.buildPrompt(params.messages); + const model = params.model || this.defaultModel; + + let sessionId = ''; + let yieldedText = ''; + + for await (const message of query({ + prompt, + options: this.buildOptions(model, systemPrompt) + })) { + if (message.type === 'system' && message.subtype === 'init') { + sessionId = message.session_id; + continue; + } + + if (message.type === 'assistant') { + if (message.error) { + throw this.errorForSdkAssistantError(message.error); + } + const fullText = extractText(message.message.content); + const delta = fullText.slice(yieldedText.length); + if (delta) { + yield { content: delta, isComplete: false, id: sessionId }; + yieldedText = fullText; + } + continue; + } + + if (message.type === 'result') { + const usage = { + promptTokens: message.usage?.input_tokens ?? 0, + completionTokens: message.usage?.output_tokens ?? 0, + totalTokens: (message.usage?.input_tokens ?? 0) + (message.usage?.output_tokens ?? 0) + }; + + if (message.subtype !== 'success') { + throw new AIProviderError( + `Claude Code session ended without a successful result: ${message.subtype}`, + AIErrorType.UNKNOWN + ); + } + + yield { content: '', isComplete: true, id: sessionId, usage }; + return; + } + } + } + + public getInfo(): ProviderInfo { + return { + name: 'Claude Code', + version: '1.0.0', + models: ['sonnet', 'opus', 'haiku', 'inherit'], + maxContextLength: 200000, + supportsStreaming: true, + capabilities: { + vision: true, + functionCalling: true, + systemMessages: true, + reasoning: true, + codeGeneration: true, + subscriptionAuth: true, + requiresLocalCli: true + } + }; + } + + protected override mapProviderError(error: any): AIProviderError | null { + if (!error) return null; + const message: string = error.message || ''; + + if (message.includes('ENOENT') && message.toLowerCase().includes('claude')) { + return new AIProviderError( + 'Claude Code CLI not found. Install it from https://docs.anthropic.com/claude/docs/claude-code and run `claude login` for subscription accounts.', + AIErrorType.INVALID_REQUEST, + undefined, + error + ); + } + + if (message.includes('not logged in') || message.includes('authentication')) { + return new AIProviderError( + 'Claude Code is not authenticated. Run `claude login` or set ANTHROPIC_API_KEY in the environment.', + AIErrorType.AUTHENTICATION, + undefined, + error + ); + } + + return null; + } + + private buildPrompt(messages: AIMessage[]): { systemPrompt: string | undefined; prompt: string } { + let systemPrompt: string | undefined; + const turns: AIMessage[] = []; + + for (const msg of messages) { + if (msg.role === 'system') { + systemPrompt = systemPrompt ? `${systemPrompt}\n\n${msg.content}` : msg.content; + } else { + turns.push(msg); + } + } + + if (turns.length === 0) { + throw new AIProviderError( + 'At least one user or assistant message is required', + AIErrorType.INVALID_REQUEST + ); + } + + // Single user turn: pass the content directly. Multi-turn history is + // flattened with role labels — the Agent SDK doesn't accept a + // messages array in stateless mode, so this is the simplest faithful + // representation of the conversation. + if (turns.length === 1 && turns[0]!.role === 'user') { + return { systemPrompt, prompt: turns[0]!.content }; + } + + const flattened = turns + .map(t => `${t.role === 'assistant' ? 'Assistant' : 'Human'}: ${t.content}`) + .join('\n\n'); + return { systemPrompt, prompt: `${flattened}\n\nAssistant:` }; + } + + private buildOptions(model: string, systemPrompt: string | undefined) { + const options: Record = { + model, + maxTurns: this.maxTurns, + allowedTools: this.allowedTools + }; + if (systemPrompt) options.systemPrompt = systemPrompt; + if (this.cwd) options.cwd = this.cwd; + return options as any; + } + + private errorForSdkAssistantError(code: string): AIProviderError { + const map: Record = { + authentication_failed: { + type: AIErrorType.AUTHENTICATION, + message: 'Claude Code authentication failed. Run `claude login` or set ANTHROPIC_API_KEY.' + }, + oauth_org_not_allowed: { + type: AIErrorType.AUTHENTICATION, + message: 'Your organization is not allowed by the Claude Code OAuth policy.' + }, + billing_error: { + type: AIErrorType.AUTHENTICATION, + message: 'Billing error. Check your Anthropic account billing or subscription status.' + }, + rate_limit: { + type: AIErrorType.RATE_LIMIT, + message: 'Rate limit exceeded.' + }, + invalid_request: { + type: AIErrorType.INVALID_REQUEST, + message: 'Invalid request to Claude Code.' + }, + model_not_found: { + type: AIErrorType.MODEL_NOT_FOUND, + message: 'Requested model is not available to this Claude Code installation.' + }, + server_error: { + type: AIErrorType.NETWORK, + message: 'Claude Code server error. Try again shortly.' + }, + max_output_tokens: { + type: AIErrorType.INVALID_REQUEST, + message: 'Response exceeded the configured maxOutputTokens.' + } + }; + + const entry = map[code] ?? { type: AIErrorType.UNKNOWN, message: `Claude Code error: ${code}` }; + return new AIProviderError(entry.message, entry.type); + } +} + +function extractText(content: unknown): string { + if (!Array.isArray(content)) return ''; + let out = ''; + for (const block of content) { + if (block && typeof block === 'object' && (block as any).type === 'text') { + out += (block as any).text ?? ''; + } + } + return out; +} diff --git a/src/providers/index.ts b/src/providers/index.ts index adde67e..af76a0c 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -7,4 +7,5 @@ export { BaseAIProvider } from './base.js'; export { ClaudeProvider, type ClaudeConfig } from './claude.js'; export { OpenAIProvider, type OpenAIConfig } from './openai.js'; export { GeminiProvider, type GeminiConfig } from './gemini.js'; -export { OpenWebUIProvider, type OpenWebUIConfig } from './openwebui.js'; \ No newline at end of file +export { OpenWebUIProvider, type OpenWebUIConfig } from './openwebui.js'; +export { ClaudeCodeProvider, type ClaudeCodeConfig } from './claude-code.js'; diff --git a/src/utils/factory.ts b/src/utils/factory.ts index cf68796..b5e3dd4 100644 --- a/src/utils/factory.ts +++ b/src/utils/factory.ts @@ -6,13 +6,15 @@ import { ClaudeProvider, type ClaudeConfig } from '../providers/claude.js'; import { OpenAIProvider, type OpenAIConfig } from '../providers/openai.js'; import { GeminiProvider, type GeminiConfig } from '../providers/gemini.js'; import { OpenWebUIProvider, type OpenWebUIConfig } from '../providers/openwebui.js'; +import { ClaudeCodeProvider, type ClaudeCodeConfig } from '../providers/claude-code.js'; import { BaseAIProvider } from '../providers/base.js'; export const PROVIDER_REGISTRY = { claude: ClaudeProvider, openai: OpenAIProvider, gemini: GeminiProvider, - openwebui: OpenWebUIProvider + openwebui: OpenWebUIProvider, + 'claude-code': ClaudeCodeProvider } as const; export type ProviderType = keyof typeof PROVIDER_REGISTRY; @@ -22,6 +24,7 @@ export interface ProviderConfigMap { openai: OpenAIConfig; gemini: GeminiConfig; openwebui: OpenWebUIConfig; + 'claude-code': ClaudeCodeConfig; } export function createProvider( @@ -59,3 +62,9 @@ export function createGeminiProvider( export function createOpenWebUIProvider(config: OpenWebUIConfig): OpenWebUIProvider { return new OpenWebUIProvider(config); } + +export function createClaudeCodeProvider( + options: Partial = {} +): ClaudeCodeProvider { + return new ClaudeCodeProvider(options); +} From c8c579da8b7e1465c2476a1f0b18dfe3cc510a2c Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 21 May 2026 14:25:01 +0200 Subject: [PATCH 2/2] docs: rewrite README Full rewrite focused on what consumers actually need: install, a working snippet per provider, and the public API surface. Changes vs the previous version: - Document ClaudeCodeProvider, including the subscription-via-CLI auth path that the new provider enables. - Remove the ProviderRegistry section (the class was removed in R1). - Drop the stale Provider Comparison and Detailed Capabilities tables; vendor capabilities and model lists move too fast for a README to track. - Remove the inaccurate Zero Dependencies and Comprehensive Testing claims (post-refactor 44/91 tests need updating). - Refresh default models (Gemini 1.5 -> 2.5) and the package list to match the current build. - Add a brief Architecture note covering the base hooks introduced in R2 and the OpenWebUI strategy split from R3. README is 315 lines (was 701). --- README.md | 732 +++++++++++++----------------------------------------- 1 file changed, 173 insertions(+), 559 deletions(-) diff --git a/README.md b/README.md index 76347ea..4b252ca 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,10 @@ # Simple AI Provider -A professional, type-safe TypeScript package that provides a unified interface for multiple AI providers. Currently supports **Claude (Anthropic)**, **OpenAI**, **Google Gemini**, and **OpenWebUI** with a consistent API across all providers. +A type-safe TypeScript library with a single API surface for **Claude (Anthropic)**, **OpenAI**, **Google Gemini**, **OpenWebUI**, and **Claude Code** (subscription-friendly via local CLI). -## ✨ Features +The same `complete()` / `stream()` interface works across every provider, with consistent error types, streaming, and optional structured (typed JSON) output. -- 🔗 **Unified Interface**: Same API for Claude, OpenAI, Gemini, and OpenWebUI -- 🎯 **Type Safety**: Full TypeScript support with comprehensive type definitions -- 🚀 **Streaming Support**: Real-time response streaming for all providers -- 🛡️ **Error Handling**: Standardized error types with provider-specific details -- 🏭 **Factory Pattern**: Easy provider creation and management -- 🔧 **Configurable**: Extensive configuration options for each provider -- 📦 **Zero Dependencies**: Lightweight with minimal external dependencies -- 🌐 **Local Support**: OpenWebUI integration for local/private AI models -- 🎨 **Structured Output**: Define custom response types for type-safe AI outputs -- 🏗️ **Provider Registry**: Dynamic provider registration and creation system -- ✅ **Comprehensive Testing**: Full test coverage with Bun test framework -- 🔍 **Advanced Validation**: Input validation with detailed error messages - -## 🏗️ Architecture - -The library is built on solid design principles: - -- **Template Method Pattern**: Base provider defines the workflow, subclasses implement specifics -- **Factory Pattern**: Clean provider creation and management -- **Strategy Pattern**: Unified interface across different AI providers -- **Type Safety**: Comprehensive TypeScript support throughout -- **Error Normalization**: Consistent error handling across all providers -- **Validation First**: Input validation before processing -- **Extensibility**: Easy to add new providers via registry system - -## 🚀 Quick Start +## Install ```bash npm install simple-ai-provider @@ -37,37 +12,14 @@ npm install simple-ai-provider bun add simple-ai-provider ``` -### Basic Usage +Requires Node ≥ 18 (or Bun). TypeScript ≥ 5 is recommended as a peer dependency. + +## Quick start ```typescript -import { ClaudeProvider, OpenAIProvider, GeminiProvider, OpenWebUIProvider } from 'simple-ai-provider'; +import { ClaudeProvider } from 'simple-ai-provider'; -// Claude -const claude = new ClaudeProvider({ - apiKey: process.env.ANTHROPIC_API_KEY!, - defaultModel: 'claude-3-5-sonnet-20241022' -}); - -// OpenAI -const openai = new OpenAIProvider({ - apiKey: process.env.OPENAI_API_KEY!, - defaultModel: 'gpt-4o' -}); - -// Google Gemini -const gemini = new GeminiProvider({ - apiKey: process.env.GOOGLE_AI_API_KEY!, - defaultModel: 'gemini-1.5-flash' -}); - -// OpenWebUI (local) -const openwebui = new OpenWebUIProvider({ - apiKey: 'ollama', // Often not required - baseUrl: 'http://localhost:3000', - defaultModel: 'llama2' -}); - -// Initialize and use any provider +const claude = new ClaudeProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }); await claude.initialize(); const response = await claude.complete({ @@ -75,183 +27,88 @@ const response = await claude.complete({ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: 'Explain TypeScript in one sentence.' } ], - maxTokens: 100, - temperature: 0.7 + maxTokens: 200 }); console.log(response.content); +console.log(response.usage); // { promptTokens, completionTokens, totalTokens } ``` -## 🏭 Factory Functions +Every provider follows the same pattern: construct, `initialize()`, then `complete()` or `stream()`. -Create providers using factory functions for cleaner code: +## Providers + +### Claude (Anthropic API) ```typescript -import { createProvider, createClaudeProvider, createOpenAIProvider, createGeminiProvider, createOpenWebUIProvider } from 'simple-ai-provider'; +import { ClaudeProvider } from 'simple-ai-provider'; -// Method 1: Specific factory functions -const claude = createClaudeProvider('your-key', { defaultModel: 'claude-3-5-sonnet-20241022' }); -const openai = createOpenAIProvider('your-key', { defaultModel: 'gpt-4o' }); -const gemini = createGeminiProvider('your-key', { defaultModel: 'gemini-1.5-flash' }); -const openwebui = createOpenWebUIProvider({ apiKey: 'your-key', baseUrl: 'http://localhost:3000' }); - -// Method 2: Generic factory -const provider = createProvider('claude', { apiKey: 'your-key' }); +const claude = new ClaudeProvider({ + apiKey: process.env.ANTHROPIC_API_KEY!, + defaultModel: 'claude-3-5-sonnet-20241022', // optional + version: '2023-06-01', // optional + timeout: 30_000, // optional, ms + maxRetries: 3 // optional +}); ``` -### Provider Registry +### Claude Code (subscription via local CLI) -For dynamic provider creation and registration: +`ClaudeCodeProvider` wraps `@anthropic-ai/claude-agent-sdk`, which authenticates through the local `claude` CLI. This is the supported path for **Claude Pro / Max subscribers** who don't have a console API key. + +**Setup:** install the CLI and run `claude login` once. No API key required. ```typescript -import { ProviderRegistry, ClaudeProvider } from 'simple-ai-provider'; +import { ClaudeCodeProvider } from 'simple-ai-provider'; -// Register a custom provider -ProviderRegistry.register('my-claude', ClaudeProvider); - -// Create provider dynamically -const provider = ProviderRegistry.create('my-claude', { apiKey: 'your-key' }); - -// Check available providers -const availableProviders = ProviderRegistry.getRegisteredProviders(); -console.log(availableProviders); // ['claude', 'openai', 'gemini', 'openwebui', 'my-claude'] -``` - -## 🎨 Structured Response Types - -Define custom response types for type-safe, structured AI outputs. The library automatically parses the AI's response into your desired type. - -```typescript -import { createResponseType, createClaudeProvider } from 'simple-ai-provider'; - -// 1. Define your response type -interface ProductAnalysis { - productName: string; - priceRange: 'budget' | 'mid-range' | 'premium'; - pros: string[]; - cons: string[]; - overallRating: number; // 1-10 scale - recommendation: 'buy' | 'consider' | 'avoid'; -} - -// 2. Create a ResponseType object -const productAnalysisType = createResponseType( - 'A comprehensive product analysis with pros, cons, rating, and recommendation' -); - -// 3. Use with any provider -const claude = createClaudeProvider({ apiKey: 'your-key' }); +const claude = new ClaudeCodeProvider({}); // uses local credentials await claude.initialize(); -const response = await claude.complete({ - messages: [ - { role: 'user', content: 'Analyze the iPhone 15 Pro from a consumer perspective.' } - ], - responseType: productAnalysisType, - maxTokens: 800 -}); - -// 4. Get the fully typed and parsed response -const analysis = response.content; -console.log(`Product: ${analysis.productName}`); -console.log(`Recommendation: ${analysis.recommendation}`); -console.log(`Rating: ${analysis.overallRating}/10`); -``` - -### Key Benefits - -- **Automatic Parsing**: The AI's JSON response is automatically parsed into your specified type. -- **Type Safety**: Get fully typed responses from AI providers with IntelliSense. -- **Automatic Prompting**: System prompts are automatically generated to guide the AI. -- **Validation**: Built-in response validation and parsing logic. -- **Consistency**: Ensures AI outputs match your expected format. -- **Developer Experience**: Catch errors at compile-time instead of runtime. - -### Streaming with Response Types - -You can also use response types with streaming. The raw stream provides real-time text, and you can parse the final string once the stream is complete. - -```typescript -import { parseAndValidateResponseType } from 'simple-ai-provider'; - -const stream = claude.stream({ - messages: [{ role: 'user', content: 'Analyze the Tesla Model 3.' }], - responseType: productAnalysisType, - maxTokens: 600 -}); - -let fullResponse = ''; -for await (const chunk of stream) { - if (!chunk.isComplete) { - process.stdout.write(chunk.content); - fullResponse += chunk.content; - } else { - console.log('\n\nStream complete!'); - // Validate the complete streamed response - try { - const analysis = parseAndValidateResponseType(fullResponse, productAnalysisType); - console.log('Validation successful!'); - console.log(`Product: ${analysis.productName}`); - } catch (e) { - console.error('Validation failed:', (e as Error).message); - } - } -} -``` - -## 📝 Environment Variables - -Set up your API keys: - -```bash -# Required for respective providers -export ANTHROPIC_API_KEY="your-claude-api-key" -export OPENAI_API_KEY="your-openai-api-key" -export GOOGLE_AI_API_KEY="your-gemini-api-key" - -# OpenWebUI Bearer Token (get from Settings > Account in OpenWebUI) -export OPENWEBUI_API_KEY="your-bearer-token" -``` - -## 🔧 Provider-Specific Configuration - -### Claude Configuration - -```typescript -const claude = new ClaudeProvider({ - apiKey: 'your-api-key', - defaultModel: 'claude-3-5-sonnet-20241022', - version: '2023-06-01', - maxRetries: 3, - timeout: 30000 +const response = await claude.complete({ + messages: [{ role: 'user', content: 'Hello!' }] }); ``` -### OpenAI Configuration +You can still pass `apiKey` to override (it's set as `ANTHROPIC_API_KEY` for the SDK). Optional config: ```typescript +new ClaudeCodeProvider({ + defaultModel: 'sonnet', // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID + maxTurns: 1, // 1 for plain completion; raise for agent/tool loops + allowedTools: [], // tool names to enable (default: none) + cwd: process.cwd() // working directory for the agent +}); +``` + +**Trade-offs to know:** +- Requires `claude` CLI installed on the host. Not ideal for typical server deployments. +- Higher latency than the direct API (spawns a CLI process per request). +- Streaming yields text as the SDK emits successive assistant messages, not token-by-token deltas. + +### OpenAI + +```typescript +import { OpenAIProvider } from 'simple-ai-provider'; + const openai = new OpenAIProvider({ - apiKey: 'your-api-key', + apiKey: process.env.OPENAI_API_KEY!, defaultModel: 'gpt-4o', - organization: 'your-org-id', - project: 'your-project-id', - maxRetries: 3, - timeout: 30000 + organization: 'org-...', // optional + project: 'proj-...' // optional }); ``` -### Gemini Configuration +`baseUrl` is supported for OpenAI-compatible endpoints. + +### Gemini (Google) ```typescript +import { GeminiProvider } from 'simple-ai-provider'; + const gemini = new GeminiProvider({ - apiKey: 'your-api-key', - defaultModel: 'gemini-1.5-flash', - safetySettings: [ - { - category: 'HARM_CATEGORY_HARASSMENT', - threshold: 'BLOCK_MEDIUM_AND_ABOVE' - } - ], + apiKey: process.env.GOOGLE_AI_API_KEY!, + defaultModel: 'gemini-2.5-flash', + safetySettings: [/* SafetySetting[] from @google/genai */], generationConfig: { temperature: 0.7, topP: 0.8, @@ -261,441 +118,198 @@ const gemini = new GeminiProvider({ }); ``` -### OpenWebUI Configuration +Backed by `@google/genai` (the successor to the deprecated `@google/generative-ai`). + +### OpenWebUI (local / self-hosted) ```typescript +import { OpenWebUIProvider } from 'simple-ai-provider'; + const openwebui = new OpenWebUIProvider({ - apiKey: 'your-bearer-token', // Get from OpenWebUI Settings > Account - baseUrl: 'http://localhost:3000', // Your OpenWebUI instance - defaultModel: 'llama3.1', - useOllamaProxy: false, // Use OpenWebUI's chat API (recommended) - // useOllamaProxy: true, // Use Ollama API proxy for direct model access - dangerouslyAllowInsecureConnections: true, // For local HTTPS - timeout: 60000, // Longer timeout for local inference - maxRetries: 2 + apiKey: 'your-bearer-token', // from Settings > Account in OpenWebUI + baseUrl: 'http://localhost:3000', + defaultModel: 'llama3.1:latest', + useOllamaProxy: false, // false: OpenWebUI chat API (default) + // true: direct Ollama proxy + dangerouslyAllowInsecureConnections: true }); ``` -## 🌊 Streaming Support +`useOllamaProxy` flips between two internal strategies — the OpenAI-compatible chat completions endpoint and the direct Ollama generate endpoint. -All providers support real-time streaming: +## Streaming + +Identical shape across providers: ```typescript -const stream = provider.stream({ - messages: [{ role: 'user', content: 'Count from 1 to 10' }], - maxTokens: 100 -}); - -for await (const chunk of stream) { +for await (const chunk of provider.stream({ messages, maxTokens: 200 })) { if (!chunk.isComplete) { process.stdout.write(chunk.content); } else { - console.log('\nDone! Usage:', chunk.usage); + console.log('\n', chunk.usage); } } ``` -## 🔀 Multi-Provider Usage +## Structured output -Use multiple providers seamlessly: +Ask any provider for a typed JSON response. The library injects a system prompt describing the expected shape and parses the result. ```typescript -const providers = { - claude: new ClaudeProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }), - openai: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! }), - gemini: new GeminiProvider({ apiKey: process.env.GOOGLE_AI_API_KEY! }), - openwebui: new OpenWebUIProvider({ - apiKey: 'ollama', - baseUrl: 'http://localhost:3000' - }) -}; +import { createResponseType } from 'simple-ai-provider'; -// Initialize all providers -await Promise.all(Object.values(providers).map(p => p.initialize())); - -// Use the same interface for all -const prompt = { - messages: [{ role: 'user', content: 'Hello!' }], - maxTokens: 50 -}; - -for (const [name, provider] of Object.entries(providers)) { - try { - const response = await provider.complete(prompt); - console.log(`${name}: ${response.content}`); - } catch (error) { - console.log(`${name} failed: ${error.message}`); - } +interface UserProfile { + name: string; + age: number; + hobbies: string[]; } + +const profileType = createResponseType( + 'A user profile with name, age, and hobbies', + { name: 'Alice', age: 30, hobbies: ['climbing', 'photography'] } +); + +const response = await claude.complete({ + messages: [{ role: 'user', content: 'Generate a fictional user profile.' }], + responseType: profileType +}); + +// response.content is typed as UserProfile +// response.rawContent is the original string +console.log(response.content.name); ``` -## 📊 Provider Comparison +This also works with `stream()` — chunks deliver text, then the final chunk parses. -| Provider | Context Length | Streaming | Vision | Function Calling | Local Execution | Best For | -|----------|---------------|-----------|--------|------------------|-----------------|----------| -| **Claude** | 200K tokens | ✅ | ✅ | ✅ | ❌ | Reasoning, Analysis, Code Review | -| **OpenAI** | 128K tokens | ✅ | ✅ | ✅ | ❌ | General Purpose, Function Calling | -| **Gemini** | 1M tokens | ✅ | ✅ | ✅ | ❌ | Large Documents, Multimodal | -| **OpenWebUI** | 32K tokens | ✅ | ❌ | ❌ | ✅ | Privacy, Custom Models, Local | +## Factory functions -### Detailed Capabilities - -Each provider offers unique capabilities: - -#### Claude (Anthropic) - -- Advanced reasoning and analysis -- Excellent code review capabilities -- Strong safety features -- System message support - -#### OpenAI - -- Broad model selection -- Function calling support -- JSON mode for structured outputs -- Vision capabilities - -#### Gemini (Google) - -- Largest context window (1M tokens) -- Multimodal capabilities -- Cost-effective pricing -- Strong multilingual support - -#### OpenWebUI - -- Complete privacy (local execution) -- Custom model support -- No API costs -- RAG (Retrieval Augmented Generation) support - -## 🎯 Model Selection - -### Getting Available Models - -Instead of maintaining a static list, you can programmatically get available models: +If you prefer a single entry point: ```typescript -// Get provider information including available models -const info = provider.getInfo(); -console.log('Available models:', info.models); +import { createProvider } from 'simple-ai-provider'; -// Example output: -// Claude: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', ...] -// OpenAI: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', ...] -// Gemini: ['gemini-1.5-flash', 'gemini-1.5-pro', ...] -// OpenWebUI: ['llama3.1:latest', 'mistral:latest', ...] +const claude = createProvider('claude', { apiKey: '…' }); +const openai = createProvider('openai', { apiKey: '…' }); +const gemini = createProvider('gemini', { apiKey: '…' }); +const openwebui = createProvider('openwebui', { apiKey: '…', baseUrl: '…' }); +const claudeCode = createProvider('claude-code', {}); ``` -### Model Selection Guidelines +Per-provider shortcut factories also exist: `createClaudeProvider`, `createOpenAIProvider`, `createGeminiProvider`, `createOpenWebUIProvider`, `createClaudeCodeProvider`. -**For Claude (Anthropic):** -- Check [Anthropic's model documentation](https://docs.anthropic.com/claude/docs/models-overview) for latest models +## Error handling -**For OpenAI:** -- Check [OpenAI's model documentation](https://platform.openai.com/docs/models) for latest models - -**For Gemini (Google):** -- Check [Google AI's model documentation](https://ai.google.dev/docs/models) for latest models - -**For OpenWebUI:** -- Models depend on your local installation -- Check your OpenWebUI instance for available models - -## 🚨 Error Handling - -The package provides comprehensive, standardized error handling with detailed error types: +Every error thrown by the library is an `AIProviderError` with a typed `.type` and optional `.statusCode`: ```typescript import { AIProviderError, AIErrorType } from 'simple-ai-provider'; try { - const response = await provider.complete({ - messages: [{ role: 'user', content: 'Hello' }] - }); + await provider.complete({ messages: [...] }); } catch (error) { if (error instanceof AIProviderError) { switch (error.type) { - case AIErrorType.AUTHENTICATION: - console.log('Invalid API key or authentication failed'); - break; - case AIErrorType.RATE_LIMIT: - console.log('Rate limited, try again later'); - break; - case AIErrorType.MODEL_NOT_FOUND: - console.log('Model not available or not found'); - break; - case AIErrorType.INVALID_REQUEST: - console.log('Invalid request parameters'); - break; - case AIErrorType.NETWORK: - console.log('Network/connection issue'); - break; - case AIErrorType.TIMEOUT: - console.log('Request timed out'); - break; - case AIErrorType.UNKNOWN: - console.log('Unknown error:', error.message); - break; - default: - console.log('Error:', error.message); + case AIErrorType.AUTHENTICATION: /* bad API key, expired token */ break; + case AIErrorType.RATE_LIMIT: /* slow down */ break; + case AIErrorType.MODEL_NOT_FOUND: /* wrong model name */ break; + case AIErrorType.INVALID_REQUEST: /* bad input */ break; + case AIErrorType.NETWORK: /* transient connectivity */ break; + case AIErrorType.TIMEOUT: /* slow request */ break; + case AIErrorType.UNKNOWN: /* fall back */ break; } - - // Access additional error details - console.log('Status Code:', error.statusCode); - console.log('Original Error:', error.originalError); + console.error(error.statusCode, error.originalError); } } ``` -### Error Types +## Configuration reference -- **AUTHENTICATION**: Invalid API keys or authentication failures -- **RATE_LIMIT**: API rate limits exceeded -- **INVALID_REQUEST**: Malformed requests or invalid parameters -- **MODEL_NOT_FOUND**: Requested model is not available -- **NETWORK**: Connection issues or server errors -- **TIMEOUT**: Request timeout exceeded -- **UNKNOWN**: Unclassified errors +All providers share these base options: -## 🔧 Advanced Usage +| Option | Type | Default | Notes | +|--------------|----------|-----------|----------------------------------------| +| `apiKey` | `string` | required¹ | ¹ Optional for `ClaudeCodeProvider` | +| `baseUrl` | `string` | provider default | Custom or self-hosted endpoint | +| `timeout` | `number` | `30000` | Request timeout in ms | +| `maxRetries` | `number` | `3` | SDK-level retry attempts | -### Custom Base URLs +Each provider adds its own config (see the sections above). + +## TypeScript + +Full type definitions ship with the package. The main types you'll use: ```typescript -// OpenAI-compatible endpoint -const customOpenAI = new OpenAIProvider({ - apiKey: 'your-key', - baseUrl: 'https://api.custom-provider.com/v1' -}); +import type { + AIMessage, + CompletionParams, + CompletionResponse, + CompletionChunk, + TokenUsage, + ProviderInfo, + ResponseType, -// Custom OpenWebUI instance -const remoteOpenWebUI = new OpenWebUIProvider({ - apiKey: 'your-key', - baseUrl: 'https://my-openwebui.example.com', - apiPath: '/api/v1' -}); + // Per-provider config interfaces + ClaudeConfig, + ClaudeCodeConfig, + OpenAIConfig, + GeminiConfig, + OpenWebUIConfig +} from 'simple-ai-provider'; ``` -### Provider Information +`CompletionResponse` is generic — when you pass `responseType`, `content` is `T` (and the original string is preserved on `rawContent`). + +## Provider metadata + +Every provider exposes `getInfo()`: ```typescript const info = provider.getInfo(); -console.log(`Provider: ${info.name} v${info.version}`); -console.log(`Models: ${info.models.join(', ')}`); -console.log(`Max Context: ${info.maxContextLength} tokens`); -console.log(`Supports Streaming: ${info.supportsStreaming}`); -console.log('Capabilities:', info.capabilities); +// { name, version, models, maxContextLength, supportsStreaming, capabilities } ``` -### OpenWebUI-Specific Features +Model lists in `info.models` are example values — refer to each vendor's docs for the current authoritative list. The library accepts any model string the underlying SDK supports. -OpenWebUI offers unique advantages for local AI deployment: +## Architecture (brief) -```typescript -const openwebui = new OpenWebUIProvider({ - apiKey: 'your-bearer-token', // Get from OpenWebUI Settings > Account - baseUrl: 'http://localhost:3000', - defaultModel: 'llama3.1', - useOllamaProxy: false, // Use chat completions API (recommended) - // Longer timeout for local inference - timeout: 120000, - // Allow self-signed certificates for local development - dangerouslyAllowInsecureConnections: true -}); +The library uses a template-method base class (`BaseAIProvider`) that owns the public lifecycle (`initialize`, `complete`, `stream`), input validation, response-type parsing, and error normalization. Each provider supplies: -// Test connection and list available models -try { - await openwebui.initialize(); - console.log('Connected to local OpenWebUI instance'); - - // Use either chat completions or Ollama proxy - const response = await openwebui.complete({ - messages: [{ role: 'user', content: 'Hello!' }], - maxTokens: 100 - }); -} catch (error) { - console.log('OpenWebUI not available:', error.message); - // Gracefully fallback to cloud providers -} -``` +- `doInitialize()` / `doComplete()` / `doStream()` — the actual SDK calls +- `getModelNamePatterns()` — regexes for naming-convention warnings +- `sendValidationProbe()` — minimal request used during `initialize()` +- `mapProviderError()` / `providerErrorMessages()` — provider-specific error translation -**OpenWebUI API Modes:** +`OpenWebUIProvider` additionally uses an internal strategy split (`OpenWebUIChatStrategy` vs `OpenWebUIOllamaStrategy`) selected by `useOllamaProxy`. -- **Chat Completions** (`useOllamaProxy: false`): OpenWebUI's native API with full features -- **Ollama Proxy** (`useOllamaProxy: true`): Direct access to Ollama API for raw model interaction - -## 📦 TypeScript Support - -Full TypeScript support with comprehensive type definitions: - -```typescript -import type { - CompletionParams, - CompletionResponse, - CompletionChunk, - ProviderInfo, - ClaudeConfig, - OpenAIConfig, - GeminiConfig, - OpenWebUIConfig, - AIMessage, - ResponseType, - TokenUsage -} from 'simple-ai-provider'; - -// Type-safe configuration -const config: ClaudeConfig = { - apiKey: 'your-key', - defaultModel: 'claude-3-5-sonnet-20241022', - // TypeScript will validate all options -}; - -// Type-safe responses -const response: CompletionResponse = await provider.complete(params); - -// Type-safe messages with metadata -const messages: AIMessage[] = [ - { - role: 'user', - content: 'Hello', - metadata: { timestamp: Date.now() } - } -]; - -// Type-safe response types -interface UserProfile { - name: string; - age: number; -} - -const responseType: ResponseType = createResponseType( - 'A user profile with name and age', - { name: 'John', age: 30 } -); -``` - -### Advanced Type Features - -- **Generic Response Types**: Type-safe structured outputs -- **Message Metadata**: Support for custom message properties -- **Provider-Specific Configs**: Type-safe configuration for each provider -- **Error Types**: Comprehensive error type definitions -- **Factory Functions**: Type-safe provider creation - -## 🧪 Testing - -The package includes comprehensive tests using Bun test framework: +## Development ```bash -# Run all tests -bun test - -# Run tests for specific provider -bun test tests/claude.test.ts -bun test tests/openai.test.ts -bun test tests/gemini.test.ts -bun test tests/openwebui.test.ts - -# Run tests with coverage -bun test --coverage -``` - -### Test Coverage - -- ✅ Provider initialization and configuration -- ✅ Message validation and conversion -- ✅ Error handling and normalization -- ✅ Response formatting -- ✅ Streaming functionality -- ✅ Structured response types -- ✅ Factory functions -- ✅ Provider registry - -## 🛠️ Development - -### Prerequisites - -- Node.js 18.0.0 or higher -- Bun (recommended) or npm/yarn -- TypeScript 5.0 or higher - -### Setup - -```bash -# Clone the repository git clone https://gitea.jleibl.net/jleibl/simple-ai-provider.git cd simple-ai-provider - -# Install dependencies bun install - -# Build the project bun run build +``` -# Run tests -bun test +Examples live in `examples/`: -# Run examples +```bash bun run examples/basic-usage.ts -bun run examples/structured-response-types.ts bun run examples/multi-provider.ts +bun run examples/structured-response-types.ts ``` -### Project Structure +Tests in `tests/` use Bun's test runner (`bun test`). Note: post-refactor some tests need updating before they pass cleanly. -```text -src/ -├── index.ts # Main entry point -├── types/ -│ └── index.ts # Type definitions and utilities -├── providers/ -│ ├── base.ts # Abstract base provider -│ ├── claude.ts # Claude provider implementation -│ ├── openai.ts # OpenAI provider implementation -│ ├── gemini.ts # Gemini provider implementation -│ ├── openwebui.ts # OpenWebUI provider implementation -│ └── index.ts # Provider exports -└── utils/ - └── factory.ts # Factory functions and registry +## License -examples/ -├── basic-usage.ts # Basic usage examples -├── structured-response-types.ts # Structured output examples -└── multi-provider.ts # Multi-provider examples +MIT — see [LICENSE](LICENSE). -tests/ -├── claude.test.ts # Claude provider tests -├── openai.test.ts # OpenAI provider tests -├── gemini.test.ts # Gemini provider tests -└── openwebui.test.ts # OpenWebUI provider tests -``` - -## 🤝 Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. - -### Development Guidelines - -1. **Code Style**: Follow the existing TypeScript patterns -2. **Testing**: Add tests for new features -3. **Documentation**: Update README for new features -4. **Type Safety**: Maintain comprehensive type definitions -5. **Error Handling**: Use standardized error types - -## 📄 License - -MIT License - see the [LICENSE](LICENSE) file for details. - -## 🔗 Links +## Links - [Anthropic Claude API](https://docs.anthropic.com/claude/reference/) +- [Claude Code SDK](https://docs.anthropic.com/claude/docs/claude-code-sdk) - [OpenAI API](https://platform.openai.com/docs/) -- [Google Gemini API](https://ai.google.dev/) +- [Google Gen AI SDK](https://ai.google.dev/) - [OpenWebUI](https://openwebui.com/) -- [Gitea Repository](https://gitea.jleibl.net/jleibl/simple-ai-provider) - ---- - -⭐ **Star this repo if you find it helpful!** +- [Repository](https://gitea.jleibl.net/jleibl/simple-ai-provider)