Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.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)
if: matrix.python-version == '3.12'
run: uv run mypy
Comment thread
Abhinavexist marked this conversation as resolved.
- 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
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
59 changes: 59 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions .github/workflows/qa-live.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
9 changes: 9 additions & 0 deletions scripts/qa_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."}],
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
11 changes: 11 additions & 0 deletions tests/test_inputs_and_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading