From d9ad590769aa63dde6ccb65091341c19481dee3b Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Tue, 21 Jul 2026 05:38:04 +0530 Subject: [PATCH 1/4] ci: GitHub Actions CI/CD (lint, types, tests, multimodal + token/usage, secret scan) + PyPI publish --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++ .github/workflows/publish.yml | 59 +++++++++++++++++++++++++++++++++ .github/workflows/qa-live.yml | 29 ++++++++++++++++ scripts/qa_live.py | 9 +++++ tests/test_chat.py | 10 ++++++ tests/test_inputs_and_client.py | 11 ++++++ 6 files changed, 161 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/qa-live.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..05ec8d6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: test (py${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + - name: Install (all extras) + run: uv sync --all-extras + - name: Lint (ruff) + run: uv run ruff check src tests scripts + - name: Type check (mypy --strict) + run: uv run mypy + - name: Unit tests + run: uv run pytest -q + + secret-scan: + name: secret scan (gitleaks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Scan working tree for leaked tokens/secrets + run: | + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz" | tar -xz gitleaks + ./gitleaks dir . --redact --no-banner diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3b1cd98 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,59 @@ +name: Publish + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: "3.12" + - name: Build sdist + wheel + run: uv build + - name: Verify metadata + run: uvx twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + testpypi: + needs: build + if: github.event.release.prerelease == true + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/interfaze + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + pypi: + needs: build + if: github.event.release.prerelease == false + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/interfaze + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/qa-live.yml b/.github/workflows/qa-live.yml new file mode 100644 index 0000000..1099376 --- /dev/null +++ b/.github/workflows/qa-live.yml @@ -0,0 +1,29 @@ +name: Live QA + +# Live e2e against the real API; gated on the INTERFAZE_API_KEY secret (never on PRs). + +on: + workflow_dispatch: + schedule: + - cron: "0 6 * * 1" # weekly, Monday 06:00 UTC + +concurrency: + group: live-qa + cancel-in-progress: true + +jobs: + live-qa: + name: live QA (multimodal + token usage + tasks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: "3.12" + enable-cache: true + - name: Install + run: uv sync + - name: Run live QA + env: + INTERFAZE_API_KEY: ${{ secrets.INTERFAZE_API_KEY }} + run: uv run python scripts/qa_live.py diff --git a/scripts/qa_live.py b/scripts/qa_live.py index 7f839e9..92cc5c3 100644 --- a/scripts/qa_live.py +++ b/scripts/qa_live.py @@ -68,6 +68,14 @@ def text_generation(): return f"vcache={r.vcache}" +def token_usage(): + r = client.chat.completions.create(messages=[{"role": "user", "content": "Say hi."}], max_tokens=30) + u = r.usage + _assert(u is not None, "no usage object") + _assert(u.prompt_tokens > 0 and u.completion_tokens > 0 and u.total_tokens > 0, "zero token counts") + return f"prompt={u.prompt_tokens} completion={u.completion_tokens} total={u.total_tokens}" + + def structured_output(): r = client.chat.completions.create( messages=[{"role": "user", "content": "Give a greeting and the number 3."}], @@ -187,6 +195,7 @@ async def go(): check("text generation", text_generation) +check("token usage", token_usage) check("structured output", structured_output) check("json_object fence stripped", json_object_fence) check("tools -> content None", tools_content_none) diff --git a/tests/test_chat.py b/tests/test_chat.py index 4b097ca..7fe6e9f 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -177,6 +177,16 @@ def test_tools_content_none(): assert r.choices[0].message.tool_calls[0].function.name == "get_weather" +@respx.mock +def test_usage_tokens_surfaced(): + mock_json(BASIC) + r = Interfaze(api_key="t").chat.completions.create(messages=[{"role": "user", "content": "hi"}]) + assert r.usage is not None + assert r.usage.prompt_tokens == 5 + assert r.usage.completion_tokens == 3 + assert r.usage.total_tokens == 8 + + # ---- async ---- @respx.mock def test_async_mapping(): diff --git a/tests/test_inputs_and_client.py b/tests/test_inputs_and_client.py index 47ff618..2cf7235 100644 --- a/tests/test_inputs_and_client.py +++ b/tests/test_inputs_and_client.py @@ -50,6 +50,17 @@ def test_audio_rejects_blacklisted_data_uri(): inputs.audio("data:image/gif;base64,AAAA") +def test_video_uses_file_part(): + part = inputs.video("https://x.com/clip.mp4") + assert part["type"] == "file" and part["file"]["file_data"] == "https://x.com/clip.mp4" + + +def test_base64_image_part(): + url = inputs.data_url(b"\x89PNG\r\n", "image/png") + part = inputs.image(url) + assert part["type"] == "image_url" and part["image_url"]["url"].startswith("data:image/png;base64,") + + def test_gif_rejected(): with pytest.raises(InterfazeError): inputs.image("https://x.com/a.gif") From cb6bd2d7d6d360f989942764cdc0e59daf95b597 Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Tue, 21 Jul 2026 06:28:49 +0530 Subject: [PATCH 2/4] ci: drop py3.9 (langchain needs 3.10+), run mypy once on 3.12 leg (review #17) --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05ec8d6..b0c3663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 @@ -28,6 +28,7 @@ jobs: - name: Lint (ruff) run: uv run ruff check src tests scripts - name: Type check (mypy --strict) + if: matrix.python-version == '3.12' run: uv run mypy - name: Unit tests run: uv run pytest -q From cfe48f0865f932fb7a55b4246b2fc0c85004e3aa Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Wed, 22 Jul 2026 00:53:04 +0530 Subject: [PATCH 3/4] build: bump requires-python >=3.8 -> >=3.9 (openai 2.x dropped 3.8) before first PyPI release (review #17) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 618d0df..7f195d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "interfaze" version = "0.1.0" description = "Official Interfaze SDK." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "MIT" } authors = [{ name = "Interfaze" }] keywords = ["interfaze", "openai", "ai", "ocr", "speech-to-text", "web-search", "sdk"] From 0b53faa0851a932135446882e72646bd3f068934 Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Wed, 22 Jul 2026 01:17:36 +0530 Subject: [PATCH 4/4] ci: add core-only py3.9 leg (langchain skipped) so declared >=3.9 support is exercised (review #17) --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0c3663..687c690 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,23 @@ jobs: - name: Unit tests run: uv run pytest -q + test-core-py39: + name: test (py3.9, core only) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: "3.9" + enable-cache: true + # langchain-openai is gated python_version>='3.10', so it's skipped on 3.9 and its tests no-op. + - name: Install (dev extras; langchain skipped on 3.9) + run: uv sync --extra dev + - name: Lint (ruff) + run: uv run ruff check src tests scripts + - name: Unit tests + run: uv run pytest -q + secret-scan: name: secret scan (gitleaks) runs-on: ubuntu-latest