diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3271f3f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,106 @@ +# Contributing to Bytez + +Thank you for your interest in contributing to Bytez! This guide will help you get started. + +## Development Setup + +### Prerequisites + +- Node.js 18+ (for JavaScript SDK) +- Python 3.8+ (for Python SDK) +- Julia 1.10+ (for Julia SDK) + +### JavaScript SDK + +```bash +cd sdk/javascript +yarn install # or npm install +``` + +**Run tests:** + +```bash +# Unit tests (fast, no API key required) +npm run test:unit + +# Integration tests (requires BYTEZ_KEY env var) +npm test +``` + +**Build:** + +```bash +npm run build +``` + +### Python SDK + +```bash +cd sdk/python +pip install -e . +pip install requests +``` + +**Run tests:** + +```bash +python test.py +``` + +### Documentation + +The documentation site uses Mintlify. To preview locally: + +```bash +cd docs +yarn install +yarn start +``` + +## Pull Request Guidelines + +### Before You Start + +1. Check existing issues and PRs to avoid duplicate work +2. For significant changes, open an issue first to discuss the approach +3. Keep PRs focused—one logical change per PR + +### Code Quality + +- **JavaScript/TypeScript:** Follow existing code style, run `npm run lint` +- **Python:** Follow PEP 8, keep code simple and readable +- **Tests:** Add unit tests for new utilities, update integration tests if changing SDK behavior +- **Documentation:** Update relevant `.mdx` files if changing public APIs + +### Commit Messages + +Use clear, descriptive commit messages: + +``` +Fix query string bug in list.models() + +- Use URLSearchParams for proper encoding +- Add unit tests for edge cases +- Fixes issue where both task and modelId produced invalid URLs +``` + +### PR Checklist + +- [ ] Code follows existing style +- [ ] Tests added/updated +- [ ] Documentation updated (if needed) +- [ ] No unnecessary changes (formatting, whitespace) +- [ ] Commit history is clean + +## Testing Philosophy + +- **Unit tests:** Fast, isolated, test pure functions without I/O +- **Integration tests:** Test real API calls, but should be optional (require API key) +- **Add unit tests when possible:** Helps catch regressions quickly + +## Questions? + +- Open an issue for questions about contributing +- Join our [Discord](https://discord.com/invite/Z723PfCFWf) for real-time discussion + +We appreciate your contributions! diff --git a/sdk/javascript/package.json b/sdk/javascript/package.json index 40aa7bf..9a4b58e 100644 --- a/sdk/javascript/package.json +++ b/sdk/javascript/package.json @@ -13,6 +13,7 @@ "build": "pkgroll", "dev": "tsx src/index.node.ts", "test": "tsx ./tests/tasks.test.ts", + "test:unit": "node --test 'src/**/*.test.ts'", "lint": "eslint ./src/*.ts", "update": "yarn upgrade-interactive" }, diff --git a/sdk/javascript/src/index.ts b/sdk/javascript/src/index.ts index 4e44348..61efb19 100644 --- a/sdk/javascript/src/index.ts +++ b/sdk/javascript/src/index.ts @@ -3,6 +3,8 @@ import Model from "./model"; // interfaces import { ListModels } from "./interface/List"; import { Response } from "./interface/Client"; +// utils +import { buildListModelsPath } from "./utils/query"; /** * API Client for interfacing with the Bytez API. @@ -16,11 +18,7 @@ export default class Bytez { list = { /** Lists available models, and provides basic information about each one, such as RAM required */ models: (options?: ListModels): Promise => - this.#client.request( - `list/models${options?.task ? `?task=${options.task}` : ""}${ - options?.modelId ? `?modelId=${options.modelId}` : "" - }` - ) as Promise, + this.#client.request(buildListModelsPath(options)) as Promise, /** List available tasks */ tasks: (): Promise => this.#client.request("list/tasks") as Promise diff --git a/sdk/javascript/src/utils/query.test.ts b/sdk/javascript/src/utils/query.test.ts new file mode 100644 index 0000000..fcb7b14 --- /dev/null +++ b/sdk/javascript/src/utils/query.test.ts @@ -0,0 +1,46 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { buildListModelsPath } from "./query"; + +describe("buildListModelsPath", () => { + it("returns base path when no options provided", () => { + assert.equal(buildListModelsPath(), "list/models"); + assert.equal(buildListModelsPath({}), "list/models"); + }); + + it("adds single query parameter correctly", () => { + assert.equal( + buildListModelsPath({ task: "chat" }), + "list/models?task=chat" + ); + + assert.equal( + buildListModelsPath({ modelId: "openai-community/gpt2" }), + "list/models?modelId=openai-community%2Fgpt2" + ); + }); + + it("adds multiple query parameters with & separator", () => { + const result = buildListModelsPath({ + task: "text-generation", + modelId: "openai-community/gpt2" + }); + + // URLSearchParams guarantees deterministic order in modern environments + assert.match(result, /^list\/models\?/); + assert.match(result, /task=text-generation/); + assert.match(result, /modelId=openai-community%2Fgpt2/); + assert.match(result, /&/); + + // Ensure we don't have double question marks (the bug this fixes) + assert.equal(result.split("?").length - 1, 1, "Should have exactly one '?'"); + }); + + it("URL-encodes special characters", () => { + const result = buildListModelsPath({ + modelId: "org/model with spaces" + }); + + assert.match(result, /modelId=org%2Fmodel\+with\+spaces/); + }); +}); diff --git a/sdk/javascript/src/utils/query.ts b/sdk/javascript/src/utils/query.ts new file mode 100644 index 0000000..a280431 --- /dev/null +++ b/sdk/javascript/src/utils/query.ts @@ -0,0 +1,28 @@ +import { ListModels } from "../interface/List"; + +/** + * Build the path for `list/models` with proper query-string encoding. + * + * Isolated into a pure function for: + * - Unit testability without mocking HTTP + * - Clear separation of concerns (URL construction vs. network) + * - Easy reasoning about edge cases + */ +export function buildListModelsPath(options?: ListModels): string { + const base = "list/models"; + + if (!options) return base; + + const params: Record = {}; + + if (options.task) params.task = String(options.task); + if (options.modelId) params.modelId = String(options.modelId); + + const entries = Object.entries(params); + + if (entries.length === 0) return base; + + const search = new URLSearchParams(entries).toString(); + + return `${base}?${search}`; +}