diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5476981..d8acea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/sambanova-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/sambanova-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/sambanova-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/sambanova-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d1125ce..51be636 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index c0aaff6..142effb 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'sambanova/sambanova-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab664..2a8f4ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index b1aaee8..78df2c8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sambanova%2Fsambanova-9c679c857c1beb7f46190458954d877fae7609d97c75ce6cef3535d996ab5100.yml -openapi_spec_hash: 4721dc38b36b424239a22f88376ef2ab +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sambanova%2Fsambanova-cdbd9deeca1f7f8371d28b4924a9ebc3c40c835f23baf56484db74df4d169438.yml +openapi_spec_hash: 102cac21393e558553834f38ed92ea76 config_hash: 94afac70f4c1c1de589f69996bb63262 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab5307..aa8f9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## 1.3.0 (2026-01-23) + +Full Changelog: [v1.2.0...v1.3.0](https://github.com/sambanova/sambanova-python/compare/v1.2.0...v1.3.0) + +### Features + +* **client:** add support for binary request streaming ([fd6244a](https://github.com/sambanova/sambanova-python/commit/fd6244ae60fe2df91a2b465cc06b2256407bc38c)) + + +### Bug Fixes + +* **api:** minor openAPI spec fixes ([5e39cbb](https://github.com/sambanova/sambanova-python/commit/5e39cbb4c4ca8fbc35133a9b6553ad7ca55dfae9)) +* compat with Python 3.14 ([f8d0a17](https://github.com/sambanova/sambanova-python/commit/f8d0a17b8d285ffb31c1241ff4c0ed966115d237)) +* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([80b2f0e](https://github.com/sambanova/sambanova-python/commit/80b2f0ebd03e43a4e5d97392c474a7a57179cb94)) +* ensure streams are always closed ([b057502](https://github.com/sambanova/sambanova-python/commit/b057502367a9313b396068b33cdcf42925bdec9a)) +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([7f5d893](https://github.com/sambanova/sambanova-python/commit/7f5d8930d54299226a83ebaeff192739da87c98a)) +* use async_to_httpx_files in patch method ([d33246e](https://github.com/sambanova/sambanova-python/commit/d33246edf84f1ec15a52bea24725f0f84a148d16)) + + +### Chores + +* add missing docstrings ([019eba0](https://github.com/sambanova/sambanova-python/commit/019eba07481acc8717f40a10c3109e494d2e4eac)) +* **ci:** upgrade `actions/github-script` ([77ed781](https://github.com/sambanova/sambanova-python/commit/77ed7817c1f0a6179e63d141fcc97b79f971af5d)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([fce9064](https://github.com/sambanova/sambanova-python/commit/fce906481b815306c766baf5cb326a01d5ddb5b8)) +* **docs:** use environment variables for authentication in code snippets ([83ff043](https://github.com/sambanova/sambanova-python/commit/83ff0430dac4e91173d28bedfb3af1daa13106bb)) +* **internal:** add `--fix` argument to lint script ([a8f92ca](https://github.com/sambanova/sambanova-python/commit/a8f92ca4dade631b727d2e8111291080e632b7f3)) +* **internal:** add missing files argument to base client ([c4d570a](https://github.com/sambanova/sambanova-python/commit/c4d570afae505d0e06b3f946dd5f3360fd208fb2)) +* **internal:** codegen related update ([89dd766](https://github.com/sambanova/sambanova-python/commit/89dd7669d6a52745dbed8bb926ac30aa016480b3)) +* **internal:** codegen related update ([ba4cbb9](https://github.com/sambanova/sambanova-python/commit/ba4cbb9f303518c0c7d7349830da344be9d38e01)) +* **internal:** update `actions/checkout` version ([ec75715](https://github.com/sambanova/sambanova-python/commit/ec757150c5f6a458a6c35445cf0c5767439a1b17)) +* **package:** drop Python 3.8 support ([e619e7d](https://github.com/sambanova/sambanova-python/commit/e619e7d79ca78487c85d0f22708590582821e547)) +* speedup initial import ([4de61f3](https://github.com/sambanova/sambanova-python/commit/4de61f39aef0af76cc79c03622b8fad19c14a439)) +* update lockfile ([0b2e4c4](https://github.com/sambanova/sambanova-python/commit/0b2e4c493d6aecd212fbb3d504cd4f5301a829cb)) + ## 1.2.0 (2025-11-07) Full Changelog: [v1.1.6...v1.2.0](https://github.com/sambanova/sambanova-python/compare/v1.1.6...v1.2.0) diff --git a/LICENSE b/LICENSE index dc942ab..beb6364 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Samba Nova + Copyright 2026 Samba Nova Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 88a448d..f952ad2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/sambanova.svg?label=pypi%20(stable))](https://pypi.org/project/sambanova/) -The Samba Nova Python library provides convenient access to the Samba Nova REST API from any Python 3.8+ +The Samba Nova Python library provides convenient access to the Samba Nova REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -93,6 +93,7 @@ pip install sambanova[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from sambanova import DefaultAioHttpClient from sambanova import AsyncSambaNova @@ -100,7 +101,7 @@ from sambanova import AsyncSambaNova async def main() -> None: async with AsyncSambaNova( - api_key="My API Key", + api_key=os.environ.get("SAMBANOVA_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: completion = await client.chat.completions.create( @@ -499,7 +500,7 @@ print(sambanova.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index b7d4abb..5aed03e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,32 @@ [project] name = "sambanova" -version = "1.2.0" +version = "1.3.0" description = "The official Python library for the SambaNova API" dynamic = ["readme"] license = "Apache-2.0" authors = [ { name = "Samba Nova", email = "info@sambanova.ai" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] -requires-python = ">= 3.8" + +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -46,7 +48,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", @@ -141,7 +143,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/requirements-dev.lock b/requirements-dev.lock index 13034b3..ee9ff1a 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via sambanova -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via sambanova -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via sambanova -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,80 +63,87 @@ httpx==0.28.1 # via sambanova httpx-aiohttp==0.1.9 # via sambanova -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -mypy==1.14.1 -mypy-extensions==1.0.0 +mypy==1.17.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest -platformdirs==3.11.0 +pathspec==0.12.1 + # via mypy +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via sambanova -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via sambanova -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via sambanova # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index c539044..240f847 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via sambanova -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via sambanova async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via sambanova -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,31 +45,32 @@ httpx==0.28.1 # via sambanova httpx-aiohttp==0.1.9 # via sambanova -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via sambanova -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via sambanova -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via pydantic # via pydantic-core # via sambanova # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp diff --git a/scripts/lint b/scripts/lint index 6414daf..4e7dfe3 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import sambanova' diff --git a/src/sambanova/_base_client.py b/src/sambanova/_base_client.py index d4af864..a234936 100644 --- a/src/sambanova/_base_client.py +++ b/src/sambanova/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,9 +1282,24 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1258,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1272,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1714,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1726,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1739,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1751,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1767,9 +1854,29 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, + ) return await self.request(cast_to, opts) async def put( @@ -1778,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1792,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/sambanova/_client.py b/src/sambanova/_client.py index 638ce05..1674190 100644 --- a/src/sambanova/_client.py +++ b/src/sambanova/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import models, embeddings, completions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, SambaNovaError from ._base_client import ( @@ -29,8 +29,14 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.chat import chat -from .resources.audio import audio + +if TYPE_CHECKING: + from .resources import chat, audio, models, embeddings, completions + from .resources.models import ModelsResource, AsyncModelsResource + from .resources.chat.chat import ChatResource, AsyncChatResource + from .resources.embeddings import EmbeddingsResource, AsyncEmbeddingsResource + from .resources.audio.audio import AudioResource, AsyncAudioResource + from .resources.completions import CompletionsResource, AsyncCompletionsResource __all__ = [ "Timeout", @@ -45,14 +51,6 @@ class SambaNova(SyncAPIClient): - chat: chat.ChatResource - completions: completions.CompletionsResource - embeddings: embeddings.EmbeddingsResource - audio: audio.AudioResource - models: models.ModelsResource - with_raw_response: SambaNovaWithRawResponse - with_streaming_response: SambaNovaWithStreamedResponse - # client options api_key: str @@ -109,13 +107,43 @@ def __init__( self._default_stream_cls = Stream - self.chat = chat.ChatResource(self) - self.completions = completions.CompletionsResource(self) - self.embeddings = embeddings.EmbeddingsResource(self) - self.audio = audio.AudioResource(self) - self.models = models.ModelsResource(self) - self.with_raw_response = SambaNovaWithRawResponse(self) - self.with_streaming_response = SambaNovaWithStreamedResponse(self) + @cached_property + def chat(self) -> ChatResource: + from .resources.chat import ChatResource + + return ChatResource(self) + + @cached_property + def completions(self) -> CompletionsResource: + from .resources.completions import CompletionsResource + + return CompletionsResource(self) + + @cached_property + def embeddings(self) -> EmbeddingsResource: + from .resources.embeddings import EmbeddingsResource + + return EmbeddingsResource(self) + + @cached_property + def audio(self) -> AudioResource: + from .resources.audio import AudioResource + + return AudioResource(self) + + @cached_property + def models(self) -> ModelsResource: + from .resources.models import ModelsResource + + return ModelsResource(self) + + @cached_property + def with_raw_response(self) -> SambaNovaWithRawResponse: + return SambaNovaWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SambaNovaWithStreamedResponse: + return SambaNovaWithStreamedResponse(self) @property @override @@ -223,14 +251,6 @@ def _make_status_error( class AsyncSambaNova(AsyncAPIClient): - chat: chat.AsyncChatResource - completions: completions.AsyncCompletionsResource - embeddings: embeddings.AsyncEmbeddingsResource - audio: audio.AsyncAudioResource - models: models.AsyncModelsResource - with_raw_response: AsyncSambaNovaWithRawResponse - with_streaming_response: AsyncSambaNovaWithStreamedResponse - # client options api_key: str @@ -287,13 +307,43 @@ def __init__( self._default_stream_cls = AsyncStream - self.chat = chat.AsyncChatResource(self) - self.completions = completions.AsyncCompletionsResource(self) - self.embeddings = embeddings.AsyncEmbeddingsResource(self) - self.audio = audio.AsyncAudioResource(self) - self.models = models.AsyncModelsResource(self) - self.with_raw_response = AsyncSambaNovaWithRawResponse(self) - self.with_streaming_response = AsyncSambaNovaWithStreamedResponse(self) + @cached_property + def chat(self) -> AsyncChatResource: + from .resources.chat import AsyncChatResource + + return AsyncChatResource(self) + + @cached_property + def completions(self) -> AsyncCompletionsResource: + from .resources.completions import AsyncCompletionsResource + + return AsyncCompletionsResource(self) + + @cached_property + def embeddings(self) -> AsyncEmbeddingsResource: + from .resources.embeddings import AsyncEmbeddingsResource + + return AsyncEmbeddingsResource(self) + + @cached_property + def audio(self) -> AsyncAudioResource: + from .resources.audio import AsyncAudioResource + + return AsyncAudioResource(self) + + @cached_property + def models(self) -> AsyncModelsResource: + from .resources.models import AsyncModelsResource + + return AsyncModelsResource(self) + + @cached_property + def with_raw_response(self) -> AsyncSambaNovaWithRawResponse: + return AsyncSambaNovaWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSambaNovaWithStreamedResponse: + return AsyncSambaNovaWithStreamedResponse(self) @property @override @@ -401,39 +451,151 @@ def _make_status_error( class SambaNovaWithRawResponse: + _client: SambaNova + def __init__(self, client: SambaNova) -> None: - self.chat = chat.ChatResourceWithRawResponse(client.chat) - self.completions = completions.CompletionsResourceWithRawResponse(client.completions) - self.embeddings = embeddings.EmbeddingsResourceWithRawResponse(client.embeddings) - self.audio = audio.AudioResourceWithRawResponse(client.audio) - self.models = models.ModelsResourceWithRawResponse(client.models) + self._client = client + + @cached_property + def chat(self) -> chat.ChatResourceWithRawResponse: + from .resources.chat import ChatResourceWithRawResponse + + return ChatResourceWithRawResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.CompletionsResourceWithRawResponse: + from .resources.completions import CompletionsResourceWithRawResponse + + return CompletionsResourceWithRawResponse(self._client.completions) + + @cached_property + def embeddings(self) -> embeddings.EmbeddingsResourceWithRawResponse: + from .resources.embeddings import EmbeddingsResourceWithRawResponse + + return EmbeddingsResourceWithRawResponse(self._client.embeddings) + + @cached_property + def audio(self) -> audio.AudioResourceWithRawResponse: + from .resources.audio import AudioResourceWithRawResponse + + return AudioResourceWithRawResponse(self._client.audio) + + @cached_property + def models(self) -> models.ModelsResourceWithRawResponse: + from .resources.models import ModelsResourceWithRawResponse + + return ModelsResourceWithRawResponse(self._client.models) class AsyncSambaNovaWithRawResponse: + _client: AsyncSambaNova + def __init__(self, client: AsyncSambaNova) -> None: - self.chat = chat.AsyncChatResourceWithRawResponse(client.chat) - self.completions = completions.AsyncCompletionsResourceWithRawResponse(client.completions) - self.embeddings = embeddings.AsyncEmbeddingsResourceWithRawResponse(client.embeddings) - self.audio = audio.AsyncAudioResourceWithRawResponse(client.audio) - self.models = models.AsyncModelsResourceWithRawResponse(client.models) + self._client = client + + @cached_property + def chat(self) -> chat.AsyncChatResourceWithRawResponse: + from .resources.chat import AsyncChatResourceWithRawResponse + + return AsyncChatResourceWithRawResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.AsyncCompletionsResourceWithRawResponse: + from .resources.completions import AsyncCompletionsResourceWithRawResponse + + return AsyncCompletionsResourceWithRawResponse(self._client.completions) + + @cached_property + def embeddings(self) -> embeddings.AsyncEmbeddingsResourceWithRawResponse: + from .resources.embeddings import AsyncEmbeddingsResourceWithRawResponse + + return AsyncEmbeddingsResourceWithRawResponse(self._client.embeddings) + + @cached_property + def audio(self) -> audio.AsyncAudioResourceWithRawResponse: + from .resources.audio import AsyncAudioResourceWithRawResponse + + return AsyncAudioResourceWithRawResponse(self._client.audio) + + @cached_property + def models(self) -> models.AsyncModelsResourceWithRawResponse: + from .resources.models import AsyncModelsResourceWithRawResponse + + return AsyncModelsResourceWithRawResponse(self._client.models) class SambaNovaWithStreamedResponse: + _client: SambaNova + def __init__(self, client: SambaNova) -> None: - self.chat = chat.ChatResourceWithStreamingResponse(client.chat) - self.completions = completions.CompletionsResourceWithStreamingResponse(client.completions) - self.embeddings = embeddings.EmbeddingsResourceWithStreamingResponse(client.embeddings) - self.audio = audio.AudioResourceWithStreamingResponse(client.audio) - self.models = models.ModelsResourceWithStreamingResponse(client.models) + self._client = client + + @cached_property + def chat(self) -> chat.ChatResourceWithStreamingResponse: + from .resources.chat import ChatResourceWithStreamingResponse + + return ChatResourceWithStreamingResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.CompletionsResourceWithStreamingResponse: + from .resources.completions import CompletionsResourceWithStreamingResponse + + return CompletionsResourceWithStreamingResponse(self._client.completions) + + @cached_property + def embeddings(self) -> embeddings.EmbeddingsResourceWithStreamingResponse: + from .resources.embeddings import EmbeddingsResourceWithStreamingResponse + + return EmbeddingsResourceWithStreamingResponse(self._client.embeddings) + + @cached_property + def audio(self) -> audio.AudioResourceWithStreamingResponse: + from .resources.audio import AudioResourceWithStreamingResponse + + return AudioResourceWithStreamingResponse(self._client.audio) + + @cached_property + def models(self) -> models.ModelsResourceWithStreamingResponse: + from .resources.models import ModelsResourceWithStreamingResponse + + return ModelsResourceWithStreamingResponse(self._client.models) class AsyncSambaNovaWithStreamedResponse: + _client: AsyncSambaNova + def __init__(self, client: AsyncSambaNova) -> None: - self.chat = chat.AsyncChatResourceWithStreamingResponse(client.chat) - self.completions = completions.AsyncCompletionsResourceWithStreamingResponse(client.completions) - self.embeddings = embeddings.AsyncEmbeddingsResourceWithStreamingResponse(client.embeddings) - self.audio = audio.AsyncAudioResourceWithStreamingResponse(client.audio) - self.models = models.AsyncModelsResourceWithStreamingResponse(client.models) + self._client = client + + @cached_property + def chat(self) -> chat.AsyncChatResourceWithStreamingResponse: + from .resources.chat import AsyncChatResourceWithStreamingResponse + + return AsyncChatResourceWithStreamingResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.AsyncCompletionsResourceWithStreamingResponse: + from .resources.completions import AsyncCompletionsResourceWithStreamingResponse + + return AsyncCompletionsResourceWithStreamingResponse(self._client.completions) + + @cached_property + def embeddings(self) -> embeddings.AsyncEmbeddingsResourceWithStreamingResponse: + from .resources.embeddings import AsyncEmbeddingsResourceWithStreamingResponse + + return AsyncEmbeddingsResourceWithStreamingResponse(self._client.embeddings) + + @cached_property + def audio(self) -> audio.AsyncAudioResourceWithStreamingResponse: + from .resources.audio import AsyncAudioResourceWithStreamingResponse + + return AsyncAudioResourceWithStreamingResponse(self._client.audio) + + @cached_property + def models(self) -> models.AsyncModelsResourceWithStreamingResponse: + from .resources.models import AsyncModelsResourceWithStreamingResponse + + return AsyncModelsResourceWithStreamingResponse(self._client.models) Client = SambaNova diff --git a/src/sambanova/_models.py b/src/sambanova/_models.py index 6a3cd1d..29070e0 100644 --- a/src/sambanova/_models.py +++ b/src/sambanova/_models.py @@ -2,7 +2,21 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +import weakref +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -256,15 +270,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -272,16 +287,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -298,6 +321,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -314,15 +339,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -354,6 +381,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, @@ -573,6 +604,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +649,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +704,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details @@ -765,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -783,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/sambanova/_streaming.py b/src/sambanova/_streaming.py index 6c5a073..30c713f 100644 --- a/src/sambanova/_streaming.py +++ b/src/sambanova/_streaming.py @@ -54,30 +54,31 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - if sse.data.startswith("[DONE]"): - break - - if sse.event is None: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - if sse.event == "error": - body = sse.data - - try: - body = sse.json() - err_msg = f"{body}" - except Exception: - err_msg = sse.data or f"Error code: {response.status_code}" - - raise self._client._make_status_error( - err_msg, - body=body, - response=self.response, - ) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + if sse.event == "error": + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -136,30 +137,31 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - if sse.data.startswith("[DONE]"): - break - - if sse.event is None: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - if sse.event == "error": - body = sse.data - - try: - body = sse.json() - err_msg = f"{body}" - except Exception: - err_msg = sse.data or f"Error code: {response.status_code}" - - raise self._client._make_status_error( - err_msg, - body=body, - response=self.response, - ) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + if sse.event == "error": + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self diff --git a/src/sambanova/_types.py b/src/sambanova/_types.py index 828c756..922cb74 100644 --- a/src/sambanova/_types.py +++ b/src/sambanova/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, @@ -243,6 +252,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +263,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case diff --git a/src/sambanova/_utils/_sync.py b/src/sambanova/_utils/_sync.py index ad7ec71..f6027c1 100644 --- a/src/sambanova/_utils/_sync.py +++ b/src/sambanova/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: diff --git a/src/sambanova/_version.py b/src/sambanova/_version.py index 08b4537..2bb9ceb 100644 --- a/src/sambanova/_version.py +++ b/src/sambanova/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "sambanova" -__version__ = "1.2.0" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version diff --git a/src/sambanova/resources/chat/completions.py b/src/sambanova/resources/chat/completions.py index dcae10b..0e1009c 100644 --- a/src/sambanova/resources/chat/completions.py +++ b/src/sambanova/resources/chat/completions.py @@ -147,7 +147,8 @@ def create( n: This is not yet supported by our models. How many chat completion choices to generate for each input message. - parallel_tool_calls: Whether to enable parallel function calling during tool use. + parallel_tool_calls: Whether to enable parallel function calling during tool use, This is not yet + supported by our models. presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to @@ -325,7 +326,8 @@ def create( n: This is not yet supported by our models. How many chat completion choices to generate for each input message. - parallel_tool_calls: Whether to enable parallel function calling during tool use. + parallel_tool_calls: Whether to enable parallel function calling during tool use, This is not yet + supported by our models. presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to @@ -498,7 +500,8 @@ def create( n: This is not yet supported by our models. How many chat completion choices to generate for each input message. - parallel_tool_calls: Whether to enable parallel function calling during tool use. + parallel_tool_calls: Whether to enable parallel function calling during tool use, This is not yet + supported by our models. presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to @@ -793,7 +796,8 @@ async def create( n: This is not yet supported by our models. How many chat completion choices to generate for each input message. - parallel_tool_calls: Whether to enable parallel function calling during tool use. + parallel_tool_calls: Whether to enable parallel function calling during tool use, This is not yet + supported by our models. presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to @@ -971,7 +975,8 @@ async def create( n: This is not yet supported by our models. How many chat completion choices to generate for each input message. - parallel_tool_calls: Whether to enable parallel function calling during tool use. + parallel_tool_calls: Whether to enable parallel function calling during tool use, This is not yet + supported by our models. presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to @@ -1144,7 +1149,8 @@ async def create( n: This is not yet supported by our models. How many chat completion choices to generate for each input message. - parallel_tool_calls: Whether to enable parallel function calling during tool use. + parallel_tool_calls: Whether to enable parallel function calling during tool use, This is not yet + supported by our models. presence_penalty: Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to diff --git a/src/sambanova/types/audio/transcription_create_params.py b/src/sambanova/types/audio/transcription_create_params.py index cf3624b..3989326 100644 --- a/src/sambanova/types/audio/transcription_create_params.py +++ b/src/sambanova/types/audio/transcription_create_params.py @@ -153,6 +153,8 @@ class TranscriptionCreateParamsBase(TypedDict, total=False): class StreamOptions(TypedDict, total=False): + """Optional settings that apply when `stream` is true.""" + include_usage: Optional[bool] """ If true and `stream` is enabled, optional usage metadata will be included in the diff --git a/src/sambanova/types/audio/transcription_response.py b/src/sambanova/types/audio/transcription_response.py index 8b330c1..b543f27 100644 --- a/src/sambanova/types/audio/transcription_response.py +++ b/src/sambanova/types/audio/transcription_response.py @@ -10,6 +10,8 @@ class TranscriptionResponse(BaseModel): + """Transcription response json object""" + text: str """audio file text transcription""" diff --git a/src/sambanova/types/audio/transcription_stream_response.py b/src/sambanova/types/audio/transcription_stream_response.py index df14925..4c06772 100644 --- a/src/sambanova/types/audio/transcription_stream_response.py +++ b/src/sambanova/types/audio/transcription_stream_response.py @@ -21,6 +21,10 @@ class ChoiceDelta(BaseModel): + """ + Transcription or translation response delta chunk generated by streamed model responses. + """ + content: Optional[str] = None """The content delta of the transcription or translation.""" @@ -83,6 +87,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceLogprobs(BaseModel): + """Completion Log Probs object""" + content: ChoiceLogprobsContent if TYPE_CHECKING: @@ -99,6 +105,10 @@ def __getattr__(self, attr: str) -> object: ... class Choice(BaseModel): + """ + Transcription or translation chunk choice generated by streamed model responses. + """ + delta: ChoiceDelta """ Transcription or translation response delta chunk generated by streamed model @@ -128,6 +138,8 @@ def __getattr__(self, attr: str) -> object: ... class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -145,6 +157,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -204,6 +220,8 @@ def __getattr__(self, attr: str) -> object: ... class TranscriptionStreamResponse(BaseModel): + """streamed chunk of a transcription response returned by the model""" + id: str """A unique identifier for the chat completion.""" diff --git a/src/sambanova/types/audio/translation_create_params.py b/src/sambanova/types/audio/translation_create_params.py index 2d3f01c..db5b1e0 100644 --- a/src/sambanova/types/audio/translation_create_params.py +++ b/src/sambanova/types/audio/translation_create_params.py @@ -153,6 +153,8 @@ class TranslationCreateParamsBase(TypedDict, total=False): class StreamOptions(TypedDict, total=False): + """Optional settings that apply when `stream` is true.""" + include_usage: Optional[bool] """ If true and `stream` is enabled, optional usage metadata will be included in the diff --git a/src/sambanova/types/audio/translation_response.py b/src/sambanova/types/audio/translation_response.py index 605fbf9..977bf51 100644 --- a/src/sambanova/types/audio/translation_response.py +++ b/src/sambanova/types/audio/translation_response.py @@ -10,6 +10,8 @@ class TranslationResponse(BaseModel): + """Translation response json object""" + text: str """audio file english text translation""" diff --git a/src/sambanova/types/audio/translation_stream_response.py b/src/sambanova/types/audio/translation_stream_response.py index 418306f..8f4fc7a 100644 --- a/src/sambanova/types/audio/translation_stream_response.py +++ b/src/sambanova/types/audio/translation_stream_response.py @@ -21,6 +21,10 @@ class ChoiceDelta(BaseModel): + """ + Transcription or translation response delta chunk generated by streamed model responses. + """ + content: Optional[str] = None """The content delta of the transcription or translation.""" @@ -83,6 +87,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceLogprobs(BaseModel): + """Completion Log Probs object""" + content: ChoiceLogprobsContent if TYPE_CHECKING: @@ -99,6 +105,10 @@ def __getattr__(self, attr: str) -> object: ... class Choice(BaseModel): + """ + Transcription or translation chunk choice generated by streamed model responses. + """ + delta: ChoiceDelta """ Transcription or translation response delta chunk generated by streamed model @@ -128,6 +138,8 @@ def __getattr__(self, attr: str) -> object: ... class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -145,6 +157,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -204,6 +220,8 @@ def __getattr__(self, attr: str) -> object: ... class TranslationStreamResponse(BaseModel): + """streamed chunk of a translation response returned by the model""" + id: str """A unique identifier for the chat completion.""" diff --git a/src/sambanova/types/chat/chat_completion_response.py b/src/sambanova/types/chat/chat_completion_response.py index a3853c9..68bc933 100644 --- a/src/sambanova/types/chat/chat_completion_response.py +++ b/src/sambanova/types/chat/chat_completion_response.py @@ -22,6 +22,8 @@ class ChoiceMessageToolCallFunction(BaseModel): + """The tool that the model called.""" + arguments: str """ The arguments to call the function with, as generated by the model in JSON @@ -73,6 +75,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceMessage(BaseModel): + """completion response message object""" + content: Optional[str] = None """The contents of the assistant message.""" @@ -138,6 +142,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceLogprobs(BaseModel): + """Completion Log Probs object""" + content: ChoiceLogprobsContent if TYPE_CHECKING: @@ -166,6 +172,7 @@ class Choice(BaseModel): """The index of the choice in the list of choices""" message: ChoiceMessage + """completion response message object""" logprobs: Optional[ChoiceLogprobs] = None """Completion Log Probs object""" @@ -184,6 +191,8 @@ def __getattr__(self, attr: str) -> object: ... class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -201,6 +210,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -260,6 +273,8 @@ def __getattr__(self, attr: str) -> object: ... class ChatCompletionResponse(BaseModel): + """chat completion response returned by the model""" + id: str """A unique identifier for the chat completion.""" diff --git a/src/sambanova/types/chat/chat_completion_stream_response.py b/src/sambanova/types/chat/chat_completion_stream_response.py index 78f780d..129782d 100644 --- a/src/sambanova/types/chat/chat_completion_stream_response.py +++ b/src/sambanova/types/chat/chat_completion_stream_response.py @@ -23,6 +23,8 @@ class ChoiceDeltaToolCallFunction(BaseModel): + """The tool that the model called.""" + arguments: str """ The arguments to call the function with, as generated by the model in JSON @@ -74,6 +76,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceDelta(BaseModel): + """completion response delta chunk generated by streamed model responses.""" + channel: Optional[str] = None """Channel (returned by reasoning models like gpt oss)""" @@ -145,6 +149,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceLogprobs(BaseModel): + """Completion Log Probs object""" + content: ChoiceLogprobsContent if TYPE_CHECKING: @@ -192,6 +198,8 @@ def __getattr__(self, attr: str) -> object: ... class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -209,6 +217,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -268,6 +280,8 @@ def __getattr__(self, attr: str) -> object: ... class ChatCompletionStreamResponse(BaseModel): + """streamed chunk of a chat completion response returned by the model""" + id: str """A unique identifier for the chat completion.""" diff --git a/src/sambanova/types/chat/completion_create_params.py b/src/sambanova/types/chat/completion_create_params.py index 82fd4d5..1275b90 100644 --- a/src/sambanova/types/chat/completion_create_params.py +++ b/src/sambanova/types/chat/completion_create_params.py @@ -27,10 +27,10 @@ "MessageToolMessageContentTextContentPartArray", "ChatTemplateKwargs", "ResponseFormat", - "ResponseFormatResponseFormatText", - "ResponseFormatResponseFormatJsonObject", "ResponseFormatResponseFormatJsonSchema", "ResponseFormatResponseFormatJsonSchemaJsonSchema", + "ResponseFormatResponseFormatJsonObject", + "ResponseFormatResponseFormatText", "StreamOptions", "ToolChoice", "ToolChoiceToolChoiceObject", @@ -138,7 +138,10 @@ class CompletionCreateParamsBase(TypedDict, total=False): """ parallel_tool_calls: Optional[bool] - """Whether to enable parallel function calling during tool use.""" + """ + Whether to enable parallel function calling during tool use, This is not yet + supported by our models. + """ presence_penalty: Optional[float] """Number between -2.0 and 2.0. @@ -335,6 +338,8 @@ class MessageAssistantMessageContentTextContentPartArrayTyped(TypedDict, total=F class MessageAssistantMessageToolCallFunctionTyped(TypedDict, total=False): + """The tool that the model called.""" + arguments: Required[str] """ The arguments to call the function with, as generated by the model in JSON @@ -416,6 +421,11 @@ class MessageToolMessageTyped(TypedDict, total=False): class ChatTemplateKwargsTyped(TypedDict, total=False): + """A dictionary of additional keyword arguments to pass into the chat template. + + Use this to provide extra context or parameters that the model's chat template can process. Keys must be strings; values may be any valid JSON type. + """ + enable_thinking: bool """ Enables the model's internal reasoning or "thinking" mode, if supported by the @@ -426,23 +436,12 @@ class ChatTemplateKwargsTyped(TypedDict, total=False): ChatTemplateKwargs: TypeAlias = Union[ChatTemplateKwargsTyped, Dict[str, object]] -class ResponseFormatResponseFormatTextTyped(TypedDict, total=False): - type: Required[Literal["text"]] - - -ResponseFormatResponseFormatText: TypeAlias = Union[ResponseFormatResponseFormatTextTyped, Dict[str, object]] - - -class ResponseFormatResponseFormatJsonObjectTyped(TypedDict, total=False): - type: Required[Literal["json_object"]] - - -ResponseFormatResponseFormatJsonObject: TypeAlias = Union[ - ResponseFormatResponseFormatJsonObjectTyped, Dict[str, object] -] +class ResponseFormatResponseFormatJsonSchemaJsonSchemaTyped(TypedDict, total=False): + """A JSON Schema definition the model's structured output. + Follows standard JSON Schema syntax. + """ -class ResponseFormatResponseFormatJsonSchemaJsonSchemaTyped(TypedDict, total=False): name: Required[str] """name of the object schema""" @@ -462,6 +461,10 @@ class ResponseFormatResponseFormatJsonSchemaJsonSchemaTyped(TypedDict, total=Fal class ResponseFormatResponseFormatJsonSchemaTyped(TypedDict, total=False): + """ + Specifies that the model should produce output conforming to a given JSON schema. + """ + json_schema: Required[ResponseFormatResponseFormatJsonSchemaJsonSchema] """A JSON Schema definition the model's structured output. @@ -475,12 +478,37 @@ class ResponseFormatResponseFormatJsonSchemaTyped(TypedDict, total=False): ResponseFormatResponseFormatJsonSchemaTyped, Dict[str, object] ] + +class ResponseFormatResponseFormatJsonObjectTyped(TypedDict, total=False): + """Specifies that the model should produce output as a raw JSON object.""" + + type: Required[Literal["json_object"]] + + +ResponseFormatResponseFormatJsonObject: TypeAlias = Union[ + ResponseFormatResponseFormatJsonObjectTyped, Dict[str, object] +] + + +class ResponseFormatResponseFormatTextTyped(TypedDict, total=False): + """Specifies that the model should produce output as plain text. + + This value is not supported yet in the chat compeltions api, if this behavior is desired do not set response format. + """ + + type: Required[Literal["text"]] + + +ResponseFormatResponseFormatText: TypeAlias = Union[ResponseFormatResponseFormatTextTyped, Dict[str, object]] + ResponseFormat: TypeAlias = Union[ - ResponseFormatResponseFormatText, ResponseFormatResponseFormatJsonObject, ResponseFormatResponseFormatJsonSchema + ResponseFormatResponseFormatJsonSchema, ResponseFormatResponseFormatJsonObject, ResponseFormatResponseFormatText ] class StreamOptionsTyped(TypedDict, total=False): + """Options for streaming response. Only set this when setting stream as true""" + include_usage: Optional[bool] """Whether to include the usage metrics in a final chunk or not""" @@ -489,6 +517,11 @@ class StreamOptionsTyped(TypedDict, total=False): class ToolChoiceToolChoiceObjectFunctionTyped(TypedDict, total=False): + """Specifies a tool the model should use. + + Use it to force the model to call that specific tool. + """ + name: Required[str] """the name of the tool expected to be used by the model""" diff --git a/src/sambanova/types/chat/general_error.py b/src/sambanova/types/chat/general_error.py index dfd0e46..c192871 100644 --- a/src/sambanova/types/chat/general_error.py +++ b/src/sambanova/types/chat/general_error.py @@ -22,4 +22,6 @@ class Error(BaseModel): class GeneralError(BaseModel): + """other kind of errors""" + error: Error diff --git a/src/sambanova/types/chat/model_output_error.py b/src/sambanova/types/chat/model_output_error.py index abed6b9..5608f33 100644 --- a/src/sambanova/types/chat/model_output_error.py +++ b/src/sambanova/types/chat/model_output_error.py @@ -27,4 +27,6 @@ class Error(BaseModel): class ModelOutputError(BaseModel): + """error in model output generation""" + error: Error diff --git a/src/sambanova/types/completion_create_params.py b/src/sambanova/types/completion_create_params.py index 535b0d8..21cb184 100644 --- a/src/sambanova/types/completion_create_params.py +++ b/src/sambanova/types/completion_create_params.py @@ -158,6 +158,8 @@ class CompletionCreateParamsBase(TypedDict, total=False): class StreamOptionsTyped(TypedDict, total=False): + """Options for streaming response. Only set this when setting stream as true""" + include_usage: Optional[bool] """Whether to include the usage metrics in a final chunk or not""" diff --git a/src/sambanova/types/completion_response.py b/src/sambanova/types/completion_response.py index 1d0e10d..4d7cf0a 100644 --- a/src/sambanova/types/completion_response.py +++ b/src/sambanova/types/completion_response.py @@ -64,6 +64,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceLogprobs(BaseModel): + """Completion Log Probs object""" + content: ChoiceLogprobsContent if TYPE_CHECKING: @@ -80,6 +82,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceMessageToolCallFunction(BaseModel): + """The tool that the model called.""" + arguments: str """ The arguments to call the function with, as generated by the model in JSON @@ -131,6 +135,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceMessage(BaseModel): + """completion response message object""" + content: Optional[str] = None """The contents of the assistant message.""" @@ -172,6 +178,7 @@ class Choice(BaseModel): """Completion Log Probs object""" message: Optional[ChoiceMessage] = None + """completion response message object""" if TYPE_CHECKING: # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a @@ -187,6 +194,8 @@ def __getattr__(self, attr: str) -> object: ... class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -204,6 +213,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -263,6 +276,8 @@ def __getattr__(self, attr: str) -> object: ... class CompletionResponse(BaseModel): + """ompletion response returned by the model""" + id: str """A unique identifier for the chat completion.""" diff --git a/src/sambanova/types/completion_stream_response.py b/src/sambanova/types/completion_stream_response.py index 231a06b..78a992e 100644 --- a/src/sambanova/types/completion_stream_response.py +++ b/src/sambanova/types/completion_stream_response.py @@ -62,6 +62,8 @@ def __getattr__(self, attr: str) -> object: ... class ChoiceLogprobs(BaseModel): + """Completion Log Probs object""" + content: ChoiceLogprobsContent if TYPE_CHECKING: @@ -109,6 +111,8 @@ def __getattr__(self, attr: str) -> object: ... class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -126,6 +130,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -185,6 +193,8 @@ def __getattr__(self, attr: str) -> object: ... class CompletionStreamResponse(BaseModel): + """streamed chunk of a completion response returned by the model""" + id: str """A unique identifier for the chat completion.""" diff --git a/src/sambanova/types/embeddings_response.py b/src/sambanova/types/embeddings_response.py index e420d71..c9db2e3 100644 --- a/src/sambanova/types/embeddings_response.py +++ b/src/sambanova/types/embeddings_response.py @@ -11,6 +11,8 @@ class Data(BaseModel): + """Represents an embedding vector returned by embeddings endpoint.""" + embedding: Optional[List[float]] = None """List of floats containing the embedding vector.""" @@ -22,6 +24,8 @@ class Data(BaseModel): class UsagePromptTokensDetails(BaseModel): + """Extra tokens details""" + cached_tokens: Optional[int] = None """amount of cached tokens""" @@ -39,6 +43,10 @@ def __getattr__(self, attr: str) -> object: ... class Usage(BaseModel): + """ + Usage metrics for the completion, embeddings,transcription or translation request + """ + acceptance_rate: Optional[float] = None """acceptance rate""" @@ -98,6 +106,8 @@ def __getattr__(self, attr: str) -> object: ... class EmbeddingsResponse(BaseModel): + """Embeddings response returned by the model""" + data: List[Data] """The list of embeddings generated by the model.""" diff --git a/src/sambanova/types/model_response.py b/src/sambanova/types/model_response.py index 33a60a1..35dab26 100644 --- a/src/sambanova/types/model_response.py +++ b/src/sambanova/types/model_response.py @@ -12,6 +12,8 @@ class Pricing(BaseModel): + """pricing details""" + completion: Optional[float] = None """price per completion token in USD""" @@ -35,6 +37,8 @@ def __getattr__(self, attr: str) -> object: ... class ModelResponse(BaseModel): + """model metadata""" + id: str """model id""" diff --git a/src/sambanova/types/models_response.py b/src/sambanova/types/models_response.py index 4b44dd2..bdf8e30 100644 --- a/src/sambanova/types/models_response.py +++ b/src/sambanova/types/models_response.py @@ -13,6 +13,8 @@ class ModelsResponse(BaseModel): + """object containing available models and metadata""" + data: List[ModelResponse] """array of model metadata list""" diff --git a/tests/api_resources/chat/test_completions.py b/tests/api_resources/chat/test_completions.py index 79ad1df..ece01ca 100644 --- a/tests/api_resources/chat/test_completions.py +++ b/tests/api_resources/chat/test_completions.py @@ -51,7 +51,28 @@ def test_method_create_with_all_params_overload_1(self, client: SambaNova) -> No parallel_tool_calls=True, presence_penalty=-2, reasoning_effort="low", - response_format={"type": "text"}, + response_format={ + "json_schema": { + "name": "User", + "description": "JSON schema for a simple user object", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the user", + }, + "name": { + "type": "string", + "description": "Full name of the user", + }, + }, + "required": ["id", "name"], + }, + "strict": False, + }, + "type": "json_schema", + }, seed=0, stop="\n", stream=False, @@ -146,7 +167,28 @@ def test_method_create_with_all_params_overload_2(self, client: SambaNova) -> No parallel_tool_calls=True, presence_penalty=-2, reasoning_effort="low", - response_format={"type": "text"}, + response_format={ + "json_schema": { + "name": "User", + "description": "JSON schema for a simple user object", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the user", + }, + "name": { + "type": "string", + "description": "Full name of the user", + }, + }, + "required": ["id", "name"], + }, + "strict": False, + }, + "type": "json_schema", + }, seed=0, stop="\n", stream_options={"include_usage": True}, @@ -245,7 +287,28 @@ async def test_method_create_with_all_params_overload_1(self, async_client: Asyn parallel_tool_calls=True, presence_penalty=-2, reasoning_effort="low", - response_format={"type": "text"}, + response_format={ + "json_schema": { + "name": "User", + "description": "JSON schema for a simple user object", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the user", + }, + "name": { + "type": "string", + "description": "Full name of the user", + }, + }, + "required": ["id", "name"], + }, + "strict": False, + }, + "type": "json_schema", + }, seed=0, stop="\n", stream=False, @@ -340,7 +403,28 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn parallel_tool_calls=True, presence_penalty=-2, reasoning_effort="low", - response_format={"type": "text"}, + response_format={ + "json_schema": { + "name": "User", + "description": "JSON schema for a simple user object", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the user", + }, + "name": { + "type": "string", + "description": "Full name of the user", + }, + }, + "required": ["id", "name"], + }, + "strict": False, + }, + "type": "json_schema", + }, seed=0, stop="\n", stream_options={"include_usage": True}, diff --git a/tests/test_client.py b/tests/test_client.py index e7e87d4..c26f7c9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -37,6 +38,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -51,6 +53,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: SambaNova | AsyncSambaNova) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -503,6 +556,70 @@ def test_multipart_repeating_array(self, client: SambaNova) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: SambaNova) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with SambaNova( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: SambaNova) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: SambaNova) -> None: class Model1(BaseModel): @@ -1375,6 +1492,72 @@ def test_multipart_repeating_array(self, async_client: AsyncSambaNova) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncSambaNova) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncSambaNova( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncSambaNova + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncSambaNova) -> None: class Model1(BaseModel): diff --git a/tests/test_models.py b/tests/test_models.py index 2e97596..d559b5c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from sambanova._utils import PropertyInfo from sambanova._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from sambanova._models import BaseModel, construct_type +from sambanova._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")