Skip to content
Closed
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,25 @@ URLs and base64 work; raw `bytes` do **not** (must be base64-encoded — this SD
- `n`, `seed`, `stop`, penalties, `logprobs`, `tool_choice`, `top_k` are ignored by Interfaze.
- The underlying OpenAI client is available at `interfaze.openai`.

## Compatibility notes

Drop-in for the OpenAI **chat completions** flow (`create`, response types, errors,
`create(stream=True)`, `models`), with a few behaviors worth knowing when migrating:

- **`.stream()` yields chunks, not events.** OpenAI's `.stream()` helper yields event objects
(`event.type == "content.delta"`); ours yields raw `ChatCompletionChunk`s (like
`create(stream=True)`) plus `get_final_completion()`. Iterate `chunk.choices[0].delta`, not
`event.type`.
- **Returned text is lightly post-processed.** `json_object` content is unwrapped from its
```` ```json ```` fence, and streamed `<think>`/`<precontext>` side-channels are pulled into
`reasoning`/`precontext`, so `message.content` may not be byte-identical to the raw wire response.
- **`inputs.*` accept https URLs** (Interfaze fetches them server-side). Those parts are valid for
Interfaze but **not** portable to OpenAI/Azure, which require base64 in `file`/`input_audio` parts.
- **Escape hatch:** anything not on the wrapper — `chat.completions.parse()`, `.with_raw_response`,
`.with_streaming_response` — is on the underlying client at `interfaze.openai`.
- **`tasks.*` return the extracted result** (a `dict`/`list`/`str`), not a `ChatCompletion` — e.g.
`tasks.ocr(...)` returns the OCR dict directly.

## License

MIT
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies = ["openai>=2,<3"]

[project.urls]
Homepage = "https://interfaze.ai"
Repository = "https://github.com/interfaze/interfaze-py"
Repository = "https://github.com/InterfazeAI/interfaze-python"

[project.optional-dependencies]
dev = ["pytest>=8", "respx>=0.21", "mypy>=1.11", "ruff>=0.6", "pydantic>=2"]
Expand Down
6 changes: 5 additions & 1 deletion src/interfaze/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ def to_interfaze(raw: ChatCompletion, strip_fence: bool) -> InterfazeChatComplet
msg["content"] = strip_json_fence(msg["content"])
except (KeyError, IndexError, TypeError):
pass
return InterfazeChatCompletion.model_validate(data)
result = InterfazeChatCompletion.model_validate(data)
request_id = getattr(raw, "_request_id", None)
if request_id is not None:
result._request_id = request_id
return result


class _CompletionsBase:
Expand Down
10 changes: 10 additions & 0 deletions src/interfaze/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def __init__(self) -> None:
self.model = ""
self.created = 0
self.tool_calls: Dict[int, Dict[str, str]] = {}
self.usage: Any = None
self.system_fingerprint: Optional[str] = None

def accumulate(self, chunk: ChatCompletionChunk) -> None:
if not self.id and chunk.id:
Expand All @@ -47,6 +49,10 @@ def accumulate(self, chunk: ChatCompletionChunk) -> None:
self.model = chunk.model
if not self.created and chunk.created:
self.created = chunk.created
if chunk.usage:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase on main; this reverts #7 and re-breaks precontext.

self.usage = chunk.usage
if chunk.system_fingerprint:
self.system_fingerprint = chunk.system_fingerprint
if not chunk.choices:
return
choice = chunk.choices[0]
Expand Down Expand Up @@ -85,6 +91,10 @@ def build(self) -> InterfazeChatCompletion:
],
"vcache": False,
}
if self.usage is not None:
data["usage"] = self.usage.model_dump()
if self.system_fingerprint:
data["system_fingerprint"] = self.system_fingerprint
if reasoning:
data["reasoning"] = reasoning
if precontext:
Expand Down
59 changes: 59 additions & 0 deletions tests/test_compliance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import asyncio

import httpx
import respx
from conftest import BASIC, CHAT_URL, _chunk, mock_sse

from interfaze import AsyncInterfaze, Interfaze

USAGE_CHUNK = {
"id": "req-test",
"object": "chat.completion.chunk",
"created": 1_700_000_000,
"model": "interfaze-beta",
"choices": [],
"usage": {"prompt_tokens": 216, "completion_tokens": 10, "total_tokens": 226},
"system_fingerprint": "fp_test",
}
STREAM_WITH_USAGE = [_chunk({"content": "Hi"}), _chunk({}, finish_reason="stop"), USAGE_CHUNK]


@respx.mock
def test_stream_captures_usage_and_fingerprint():
mock_sse(STREAM_WITH_USAGE)
stream = Interfaze(api_key="t").chat.completions.stream(
messages=[{"role": "user", "content": "x"}], stream_options={"include_usage": True}
)
final = stream.get_final_completion()
assert final.usage is not None
assert final.usage.prompt_tokens == 216
assert final.usage.completion_tokens == 10
assert final.usage.total_tokens == 226
assert final.system_fingerprint == "fp_test"


@respx.mock
def test_async_stream_captures_usage():
mock_sse(STREAM_WITH_USAGE)

async def go():
stream = AsyncInterfaze(api_key="t").chat.completions.stream(
messages=[{"role": "user", "content": "x"}], stream_options={"include_usage": True}
)
async for _ in stream:
pass
return await stream.get_final_completion()

final = asyncio.run(go())
assert final.usage is not None and final.usage.total_tokens == 226


@respx.mock
def test_request_id_carried_through():
respx.post(CHAT_URL).mock(
return_value=httpx.Response(200, json=BASIC, headers={"x-request-id": "req-abc123"})
)
r = Interfaze(api_key="t").chat.completions.create(messages=[{"role": "user", "content": "x"}])
assert r._request_id == "req-abc123"